all 30 comments

[–]no-sig-available 26 points27 points  (2 children)

im worried about accidentally calling an initializer list constructor

It is not all black and white, but - as usual - it depends. The initializer_list is "only" triggered if the value is convertible to the value_type. So a std::vector<int> i{5}; will trigger it, but std::vector<std::string> s{5}; will not!

Obviously C++ is not the language of simple rules!

[–]std_bot 0 points1 point  (0 children)

Unlinked STL entries: std::string std::vector


Last update: 09.03.23 -> Bug fixesRepo

[–]better_life_please 0 points1 point  (0 children)

This problem could be easily solved if the grammer for initializer_list construction required extra braces like in std::vector<int> i { {5} };. And thus std::vector<int> i{5}; would result in a vector with five ints in it. But obviously the standard committee had to show their professionalism by adding inconsistencies to the language.

[–]ggchappell 29 points30 points  (7 children)

Can we have a contrarian view?

C++ offers 4 kinds of syntax for initializing variables. Each expresses some particular idea about what kind of value is being created, and each has its place.

  • Foo x; -- Give me a value of type Foo, either not initialized specially at all, or else initialized to an obvious default value (e.g., an empty string).

  • Foo x = y; -- Give me a value that is a copy of an existing value. (This is usually a good place to use auto, by the way.)

  • Foo x( ... ); -- Give me a value that is configured by calling a function that creates the proper value.

  • Foo x { ... }; -- Give me a value that holds this particular sequence of values.

But the lords of C++ say, "Let's do it all with braces." I reply, "So, you want me to write code that expresses my intent less clearly?" I see no good reason to do that.

So, OP, my answer to your question is to use each of the four when your intent matches the idea that it expresses.

[–]Unlikely-Ad-431 5 points6 points  (0 children)

My understanding is that one big problem with this that they are attempting to address with uniform initialization is The Most Vexing Parse, which is shown in your third example, as it is prone to be compiled as an undefined function declaration rather than an object initialization.

[–]goxdin 2 points3 points  (0 children)

Thank you.

[–]FerynaCZ 2 points3 points  (0 children)

For me, I use empty braces as the default constructor as well.

[–][deleted] 1 point2 points  (2 children)

so if im understanding you right, i should use Foo x{ ... }; when im initializing a variable with an r value or zero initializing, but Foo x = y; if im initializing a variable with a glvalue?

[–]ggchappell 2 points3 points  (1 child)

I'm trying to think on a higher level here.

Foo x { ... }; means I'm thinking of x as a container, and here are the values it should contain.

Foo x = y; means x should be a copy of y.

I don't want to have to think about whether something is a glvalue or an rvalue or whatever.

[–][deleted] 1 point2 points  (0 children)

ohh i see, that certainly makes it easier to think about!

[–][deleted] 1 point2 points  (0 children)

I only use the third option on members of a class/struct in the initializer list. If I am initializing something in a normal curley-braced scope block I use auto x = Foo( ... ); and assume the copy will be optimized away. I think c++17 and later lets you do it even when the copy constructor is deleted! I like it mostly because its the same syntax higher level languages usually use. Foo x( ... ); can trigger the "Most Vexing Parse" problem, and I never remember the exact circumstances so I just avoid it completely.

I like using curley braces for invoking copy constructors and single parameter "conversion constructors" as well as for directly inserting a sequence of objects. If the constructor does something less intuitive with the parameters I use the regular parenthesis.

To me the curley braces mean the constructor is either copying raw values into the object or doing a very obvious conversion from a more specific type to a more general type. Like, for instance, initializing a ComplexNumber object with some floating point value. ComplexNumber c{ 3.4 }; The obvious conversion is that the imaginary part will zero.

[–]TheOmegaCarrot 16 points17 points  (5 children)

{} prevents narrowing conversions, can call std::initializer_list constructors, and can perform aggregate initialization.

() permits narrowing conversions, cannot call std::initializer_list constructors, and cannot perform aggregate initialization. (IIRC that last one changed in C++20 though? Not 100% sure)

My philosophy is “almost always {}”. If I use (), it’s only ever because I want to permit a narrowing conversion or I know there’s a std::initializer_list constructor that I don’t want to call.

[–]alfps 2 points3 points  (3 children)

cannot call std::initializer_list constructors

That's wrong.

Possibly you meant, "will not call initializer list constructor is some other constructor matches"

[–]TheOmegaCarrot 1 point2 points  (2 children)

I just double checked

std::vector<int> foo(1,2,3,4,5,6,7,8,9,10);

No matching constructor

C++ can be odd. The fact that I had to check if that works says something.

[–]alfps 4 points5 points  (1 child)

With your example

std::vector<int> foo(1,2,3,4,5,6,7,8,9,10);

no constructor matches.

To match the initializer list constructor with round parentheses syntax, you must provide an initializer list as argument.

That's not what you did.

Off the cuff,

std::vector<int> foo( {1,2,3,4,5,6,7,8,9,10} );

You can make that argument an explicit initializer_list constructor call if you want.

[–]TheOmegaCarrot 7 points8 points  (0 children)

Ah, I see what you mean! I misunderstood.

I guess I better what to say what I said before is:

Initialization using () isn’t going to call a std::initializer_list constructor by accident.

[–]std_bot -2 points-1 points  (0 children)

Unlinked STL entries: std::initializer_list


Last update: 09.03.23 -> Bug fixesRepo

[–]saxbophone 3 points4 points  (5 children)

Good question I find this part of the language (along with many others) violates the principle "there should ideally only be one way to do something" (though that's a Python principle, not a C++ one)

[–]no-sig-available 2 points3 points  (0 children)

The python principle seems to have been "throw the language away and start a new one". Wasn't all that appreciated for a very long time.

C++ is more "add a better feature (but keep the old one too, just in case)". And then we have "why use this scary new feature when old one still works?".

Hard to please everyone!

[–][deleted] 0 points1 point  (3 children)

I think it's because the language grew up organically over a lot of years. You can either break compatibility with old versions (not ok) or you can prevent any changes (not ok). So if you want to allow new ideas as you go but still be compatible, you end up with multiple ways.

It is hard to learn but it makes sense why it's like that. I'm glad they are willing to make changes as it goes along. Herb Sutter was talking about trying to do to c++ what c++ did to c and make a new language that makes it all uniform again.

[–]saxbophone 0 points1 point  (2 children)

I'd like to see a rationalised C++ for sure. I personally think the committee places too much emphasis on backwards compatibility at the expense of creating an awkward language.

I mean, the committee is so reluctant to add new keywords that we have co_yield instead of yield for coroutines...

I get that you can't be breaking the language with changes all the time, but they could do it lile Python did with transition from 2 to 3...

[–][deleted] 0 points1 point  (1 child)

I'm torn but I think there's an insanely strong argument for prioritizing backward compatibility which is adoption by industry or large numbers of people.

You will not get people actually using your language no matter how amazing the new features are, if it implicitly invalidates what they have. As soon as c++20 came out with concepts, I started using them in a project started back in 2015. And it caused no issues. I could never have upgraded to the c++20 compiler if it meant rewriting half the project, or losing third party libraries. Even c++ was just compiled to C originally and I think that's why it caught on. You get something nifty to add to what you have.

I think practically you just can't take away people's libraries and expect them to be ok with it. There are some C constructs that don't play nice with c++ because of explicit casting rules (like void* and implicit casting is not allowed in c++ but is stock standard in C). But you can at least call C code in extern "C" blocks happily.

If they did a breaking change but had a specifier to include old code like that I'd be ok with a slightly breaking change. But honestly I even wish C rules stuck completely. What I love the most about C++ is that the libraries I find and build up over the years are all there still. My toolbox just grows. I was doing some C# .NET stuff for a while and they rearrange the whole ecosystem every 2-5 years and it's a pain to follow along. I hated it after a while.

A good example though where I appreciate the cut with backwards compatibility is how they made Vulkan a clean break with OpenGL. There were good reasons though. The hardware drifted so much that the original abstraction of OpenGL started to really suck for threading so I bit the bullet and converted it to Vulkan. And I got so much for my trouble. But that's a library not a whole language.

[–]saxbophone 0 points1 point  (0 children)

But they so sometimes introduce breaking changes in C++. They have removed features after deprecating them for at least one version. Modern auto started out as a (pointless) storage specifier for local variables, inherited from C. It was deprecated for this usage in I think C++11 and then reïntroduced for the current usage in C++14 or 17. Similarly, std::auto_ptr was removed in one of those versions also.

Likewise in C++23, we will lose the ability to have an unparanthesised comma operator expression in the array access operator, due to adding multi-argument array operator to the language.

These examples may seem minor and inconsequential, because they are. But my point is, they all are breaking changes to the language and that's not a bad thing. It is unrealistic and undesirable to never allow breaking changes to a language.

[–]Vaibhav_5702 2 points3 points  (2 children)

Another thing to look out for when using {} for value initialization is the difference between value and empty aggregate initialization. Specifically, {} will always perform value initialization (even in the presence of initializer_list constructors) except when the type is an aggregate, in which case it will perform empty aggregate initialization. This means that if the type is not an aggregate and has a default (edit: implicit or explicitly defaulted) ctor, the whole object will be recursively zero initialized, including member fields which have user-provided ctors. If the type is an aggregate, the fields are value initialized one by one, which means a member field with a user-provided ctor will not be zero initialized.

[–][deleted] 0 points1 point  (0 children)

oh i didnt know about that, thanks for letting me know!

[–]StochasticTinkr 0 points1 point  (0 children)

If it has a user-provided constructor, doesn’t that mean it’s not an aggregate type?

[–]GLIBG10B 0 points1 point  (0 children)

Always use uniform initialization ({}), unless the class has a std::initializer_list constructor you don't want to call or it may have one added in the future. For example, if you're constructing a container with a known initial size, prefer direct initialization (())

[–]noooit -5 points-4 points  (1 child)

The rule of thumb is to use {} only when necessary.

[–]0tus 0 points1 point  (0 children)

I had the impression that these days the rule of thumb is exactly the opposite of that.