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.
Did you mean cannot be done with paren? because of the most vexing ?