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, 9So 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++20Doing 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 ids 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.