C++ tidbit #10: `=default` Impact on Initialization
Here's just one gotcha of dozens lurking within C++ initializations.
The following is called 'value initialization':
int a = int();
It is defined as a type name followed by a pair of empty parens. It means: if the type has a user provided default ctor, call it. Otherwise, perform 'zero-initialization'.
So when the user does not provide a default ctor:
?
struct Thingy1 { int i; };
Thingy1 t1;
cout << t1.i << endl; // Junk
cout << Thingy1().i << endl; ? // Zero
The compiler-generated ctor does nothing, leaving in `i` whatever bits happened to be on the stack at the moment. Thingy1() is not a ctor call - it is value initialization, zeroing `i`.
When the user does provide a default ctor:
struct Thingy2 {
Thingy2() {};
int i;
};
cout << Thingy2().i << endl; ? // Junk
Thingy2() now means a ctor call - and like the compiler-generated one, this particular empty ctor doesn't touch `i`.
So far so good. 'Zero initialization' actually does mostly what you think it does - the trap lies elsewhere. As you have no doubt meticulously studied previous articles in this series, you're surely alerted by the terminology 'user provided'.
struct Thingy3 {
? Thingy3() = default; // user DECLARED, not provided!
? int i;
};
cout << Thingy3().i << endl; ? // Zero again!
It is entirely reasonable to expect a special member function marked as `=default` would behave the same as a compiler-generated one. However, C++ chose (knowingly? not sure) to treat functions marked `=default` as user declared, but not provided. Thingy3() doesn't mean a ctor call (because it isn't user provided) but rather zero initialization.
Go ahead and play with it yourself .
It has been known for a while that C++ initialization is bonkers . Seriously bonkers . Timur Doumler put it best:
What worries me is that it actually keeps getting worse. Here's another Timur talk, from C++Now2022. He presents an initialization method added by C++20 which he calls 'direct aggregate initialization', spends 20 seconds describing the problem it solves and 4.5 minutes the various pitfalls it introduces and devious little differences from other initializations.
Have you heard about Microsoft's minus-100? Here are some snippets from a nearly 20yo blog by one of C# language designers :
We worked hard to keep the complexity down. One way to do that is through the concept of “minus 100 points”. Every feature starts out in the hole by 100 points, which means that it has to have a significant net positive effect on the overall package for it to make it into the language.
...
Yes, being able to [..whatever..] is somewhat more convenient than having to write the test yourself, but it doesn't really enable you to do anything new.
...
Some features provide enough utility in the abstract, but when we go to come up with a workable design for the feature, we find that we can't come up with a way to make it simple and understandable for the user.
I don't think `direct aggregate initialization` would have survived a minus-100 filter. I doubt half of C++'s initialization methods would.
It is understandably tempting to add a feature that solves a small problem because the benefit is entirely tangible and the cost isn't - but the added complexity in interaction with the myriad previous features is there even if you can't see it during design. I guess it takes some humility and maybe courage to consider such unknowns, not entirely sure I'd have coped better myself.
Director of Software Engineering @ Speedata.io | C++ Guru and Speaker | ISO C++ standardization group member
1 年I really agree with this, the number of ways to initialize things in C++ is horrific and the pitfalls that they introduce in each and every one. I hope it would be possible to deprecate many of those but I guess it won't be possible for backwards compatibility.