C++20 takes yet another swing at its infamous initialization rules. The players involved this time are Aggregate initialization (type a{1, 2, 3}
) and direct initialization (type a(1, 2, 3)
). A common pitfall with aggregate init is:
std::vector<int> vec0(5, 9); // 9, 9, 9, 9, 9
std::vector<int> vec1{5, 9}; // 5, 9
So if you don't know what you're doing, {}
is potentially dangerous to use with types that might have both "real" constructors and such with std::initializer_list
. If you had your head in the sand for 10 11 years and always used ()
then you never were in danger.
Everyone but true language masters has mental models (read: simplifications) about how initialization works. A good rule of thumb for me was always using ()
when I want to make sure I call "real" constructors, and using {}
When I want to use aggregate init or default init. The latter is necessary because default init with zero-parameter constructor can't be done with ()
.
struct aggregate{
int a=0, b=0;
}
aggregate ag0{};
aggregate ag1{1, 2};
aggregate ag2{.a=1, .b=2}; // C++20
Doing things this way had one neat property: You couldn't trigger the more powerful aggregate init when using ()
. It was a compile-time error, preventing potentially unwanted behavior. For example:
struct id{ // This is NOT an aggregate
id(int param) : m_id(param){}
private:
int m_id;
};
struct extended_id : id{ // This IS an aggregate
uint64_t m_counter;
};
int main(){
extended_id some_id(5);
return 0;
}
I made a mistake here: I forgot to add a constructor or a member initializer to extended_id
. Constructors aren't inherited in C++ for good reason: Additional members could end up not being initialized. So until recently this was an error and reported as such by the compiler.
However it compiles just fine in C++20 because ()
is aggregate-init now. That's much more powerful and leaves the additional member in a well-defined but unintended state.
This blunder could have been prevented in several ways of course. Not marking id
s constructor as explicit
is already a deadly sin. Forgetting to write a constructor, too. Some more reasons why this is not as bad as it might seem:
- All code that compiled before compiles after, and it means the same thing
- Only affects aggregates (which can have non-aggregate bases classes though)
- It did fix an otherwise unfixable problem with perfect forwarding as I understand it (see original paper)
Still, I've never read about this change in any of the numerous C++20 texts and wanted to raise awareness. I have my doubts but will adapt, like with all changes good or bad.
I think people who will suffer most from this are those already struggling with the language. Those for which C++ is a tool and not a pleasure. Who don't know what an aggregate is, have no interest in the different kinds of initialization and who may have heard about the trouble with {} and therefore never used it. They did the right thing and were never bitten by it until now.
Yes, thanks for bringing that up. Those terms are so anti-intuitive.