Skip to content

Instantly share code, notes, and snippets.

@s9w
Last active May 29, 2022 17:00
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save s9w/ad9b1dd1ea6fb17e956559c8b352e246 to your computer and use it in GitHub Desktop.
Save s9w/ad9b1dd1ea6fb17e956559c8b352e246 to your computer and use it in GitHub Desktop.
Potential issue with C++20's initialization change

Potential issue with C++20's initialization change

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 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.

compile explorer link

@kobi-ca
Copy link

kobi-ca commented May 28, 2022

The latter is necessary because default init with zero-parameter constructor [can't be done with curly braces] (https://en.wikipedia.org/wiki/Most_vexing_parse).

Did you mean cannot be done with paren? because of the most vexing ?

@s9w
Copy link
Author

s9w commented May 28, 2022

Yes, thanks for bringing that up. Those terms are so anti-intuitive.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment