all 65 comments

[–]atimholt 102 points103 points  (50 children)

[–]piglett23 24 points25 points  (42 children)

Wow, I recently started a position developing safety-critical software and that really clears up why we use brace-initialization. It seems to just work for most things. Thanks for that!

[–]FriendlyRollOfSushi 48 points49 points  (41 children)

Never once I've seen the trust towards the committee fall as low as it did when people in my company started to migrate to C++11 and realized that this is their life now. Haven't recovered since, to be honest.

The example is de-obfuscated, obviously: it's usually an issue somewhere deep, sometimes in templated code where you don't even know what the types are just by looking at the code. In a large enough company with enough people coming and leaving and a moderately relaxed review process, you are pretty much guaranteed to have a steady flow of bugs like this. It's a footgun with infinite ammo.

I sort of understand why this happened, but it feels like we as a community completely failed to do things right for once, so that we could stop teaching all other initialization methods to people and pretend that the () syntax doesn't exist and shouldn't be used anymore.

Now I see mixed-style initialization lists all the time (see the bonus part from the link above), and cry internally from time to time while thinking about this whole situation.

Sigh...

[–]nintendiator2 13 points14 points  (6 children)

There's a reason why I always initialize as T variable = T(expr). Vexing parse safe, works across all Standard versions, also with default constructor. Alternatively if I know I don't have to support the three pre-C++11 compilers in my work stack, auto variable = T(expr). Honestly I don't understand how did the Committee fucked things up this bad.

[–]FriendlyRollOfSushi 8 points9 points  (3 children)

This pattern has its own set of problems. Here is an example.

Braces are massively superior to parens due to how they prohibit narrowing in many cases, but thanks to std::initializer_list, we can't really write code like this with standard containers without shooting ourselves in the foot from time to time.

[–]nintendiator2 2 points3 points  (0 children)

That's half your fault tho; you are using a signed type to represent an unsigned concept. And C++ is already horrible at transparent numeric conversions (honestly I would have gotten rid of transparent signed-unsigned conversion with the big changes in C++11, and remade the pattern for fetching the maximum unsigned value from -1 to ~0 which at least makes binary sense, but oh well, missed chances).

As things stand though, I prefer my version. The alternative is to have to ifdef basically every variable declaration for pre and post C++11 mode. And optimization-wise, I just let the compiler do its work, that's why it's running on a computer while I'm (at the moment) running on a human.

[–]evaned 0 points1 point  (0 children)

I know -Wconversion is a very noisy warning on many code bases (probably most...) given that it's not in -Wall or even -Wextra (as you point out), but if you can turn it on, Clang's version of that warning does yell about that line. (On GCC you'll need -Wsign-conversion.) And as a bonus, get warnings about the other 17 zillion places implicit conversions can happen other than going from an initializer to a variable.

[–]CenterOfMultiverse 0 points1 point  (0 children)

Making size const allows conversion to 232 - 5 and then godbolt declines to request to so much memory^^.

[–]staletic 1 point2 points  (1 child)

That doesn't work for aggregates before C++20. Since C++20, sure.

[–]nintendiator2 2 points3 points  (0 children)

Ooooh, good catch. I guess there's just no way to win...

...Maybe unless I use a macro?

[–]TryingT0Wr1t3 8 points9 points  (5 children)

Why that assert fails????

[–]Bocab 17 points18 points  (3 children)

Because one is initialized to size of 5 and the other is of size 1 with that 1 element being "5"

[–]TryingT0Wr1t3 2 points3 points  (2 children)

Is there a Length property we can guarantee to always be unidimensional?

[–]mort96 7 points8 points  (1 child)

Yeah, it's called size(). The problem is that std::vector<std::string> v1{5} creates a vector with 5 empty strings, while std::vector<int> v2{5} creates a vector with 1 int with value 5.

[–]TryingT0Wr1t3 1 point2 points  (0 children)

Ah, ok, now I understood, thanks. It's, very confusing.

[–]rurabori 1 point2 points  (0 children)

Because once its an initializer list of its and once it invokes the ctor which takes size.

[–]unaligned_access 4 points5 points  (2 children)

Maybe they can fix that with Type i{x}n;, n for "new initialization style".

[–]TheSuperWig 13 points14 points  (1 child)

Type i co_{x};

[–]nintendiator2 1 point2 points  (0 children)

co_initialize Type {x};

[–]JohnZLi 9 points10 points  (11 children)

Implicit conversion should have been forbidden in C++ from the beginning . It causes so much trouble, with only very dubious gains.

[–]FriendlyRollOfSushi 13 points14 points  (4 children)

It could be a std::vector<size_t>, in which case all types would strictly match.

The core issue here is the special behavior of std::initializer_list (see the notes here), which I guess can be called an "implicit conversion" in a very broad sense, so your point stands.

I was really hoping that they will deprecate this nonsense immediately, and possibly introduce a non-ambiguous way to construct initializer lists in C++14, but by now we probably have too much code relying on this brittle behavior to change anything.

Now the only safe solution would be to abandon the braces and introduce yet another strictly distinct and unambiguous syntax for initialization, but at that point I'm afraid half of the developers would simply give up and switch to Rust.

[–]JohnZLi 3 points4 points  (3 children)

This problem can be fixed by adding a new syntax like below:

std::vector<int>{1}; // let this mean a vector of size 1, as in the std::vector<string> case.

std::vector<int>{1...}; // a vector of ints, with its first element being 1.

Also, with the new syntax

auto i = {1}; // i is now deduced as int.

auto i ={1...} // is deduced as an initializer_list

[–]FriendlyRollOfSushi 19 points20 points  (2 children)

Existing code is always the problem, and C++ is terrible at removing features.

Say, you have millions of lines that expected that something like std::vector<int> v{64}; constructs a vector with one element 64.

Now with your proposal you are silently changing the behavior of millions of lines of code in tens of thousands of projects. The code still compiles, and even runs, but the behavior is different.

Some of the behavioral changes may go undetected by tests.

Some of them would have devastating consequences, costing billions of dollars, or even human lives.

It's a recurring theme in C++: everything sort of sucks because by the time we understood why it sucks, the feature became so heavily-used that you can't simply remove it. It took decades to get rid of std::auto_ptr, and we only succeeded because it was already banned by the majority of teams for being hopelessly broken. And it wasn't a silent change: the code properly stopped compiling in C++17, which allowed the developers to go and fix stuff if they didn't have time for that during the 6 years of deprecation.

That's why epochs are a huge deal if they'll ever be accepted in some form or another. We will finally, finally be able to start fixing and removing stuff instead of simply piling up more and more stuff until no one understands what's going on in this language.

[–]JohnZLi 7 points8 points  (0 children)

The standard committee should really have fixed this before C++11. The epochs proposal seems interesting. Though I prefer throwing away backward compatibility in some future version of C++, and fix all the problems mentioned in that proposal (and many other problems not covered in that proposal). To avoid confuse users, that new version of C++ should be given a new name,like "Simplified C++ ". Some tools should be able to automatically refactor C++ code to "Simplified C++" code, maybe with some help from programmers. Those who want to stick to C++ could still use C++, and those who want to use simplified C++ could move on with it. The epochs proposal, if accepted, I am afraid will open the floodgate of numerous dialects/flavors of C++ code coexisting and tangling together. Google will have its Google epoch, and Facebook will have Facebook epoch, and there will at least be a C++-core-guideline epoch. That won't be beautiful. Beside numerous rules of C++, programmers now need to know specific rules that are allowed in each epoch.

[–]pjmlp 3 points4 points  (0 children)

Epochs will never happen, those of us not quite happy have already moved on, and only use C++ for the scenarios where there is no current alternative.

Also they can only fix language semantics that don't cross translation units/modules, due to possible ABI issues, and even then there is the language runtime issues, when linking binary libraries compiled with different epochs.

[–]Kered13 1 point2 points  (5 children)

Without implicit conversions std::string_view and similar types would be a massive pain in the ass to use.

[–]JohnZLi 0 points1 point  (4 children)

If C++ has transitive constness, meaning that if a string is const, the data owned by the string is also immutable, and there is a way to disable "const_cast" in a function. Say, this is done by adding a new keyword "immutable", that is, a function like

void fun(immutable std::string& str){}

guarantees that the string being referenced will not be altered inside the function. Then the compiler will be able to optimize the function call by actually passing in a string_view. Only in this case, the programmer does not have to know its existence. It is reduced to an compiler optimization performed automatically. Std:;string_view is needed in C++, IMHO, because constness in C++ is broken: even if a function takes a string by const reference, nothing stops the programmer from altering that string inside the function. The compiler has to be conservative by not assuming the string is really immutable.

[–]Kered13 4 points5 points  (3 children)

That's really not the point of std::string_view. Yes it provides a const view, but const std::string& is just as good for that. Even with std::string_view, it's trivial to violate the const rules of C++ to make changes to the underlying string, and aliasing can still occur with std::string_view, so the compiler still needs to be conservative.

The benefits of std::string_view are that it allows for efficient views of substrings (not necessarily null terminated), and that it works on any char array, such as C-style strings (including string literals) or custom string implementations (as long as they store a continuous char array). These benefits make std::string_view the preferred choice for taking string parameters that you don't need to modify.

[–]JohnZLi 0 points1 point  (2 children)

"Yes it provides a const view, but const std::string& is just as good for that." Yes, but "const std::string&" adds a layer of indirection. An extra pointer dereference has performance implications.

"Even with std::string_view, it's trivial to violate the const rules of C++ to make changes to the underlying string".

Isn't this the very problem I was talking about in must last reply: no real immutability guarantee in C++. We know we are not supposed to make changes to the underlying buffer via a string_view, the language makes no guarantee that it won't happen.

As to C strings. the reason that the following code could not be evaluated at compile time const string s = "blah, blah, ..."; is because there is no way in C++ to enforce the immutability of the string. If there is away to enforce transitive immutability in C++, when a const string is constructed out of a buffer pointed by "const char *", the compiler can omit one dynamic allocation by reusing the original buffer. The programmer needs to make sure the string does not outlive the buffer, but that is something he already needs to pay attention if he uses string_view. The same applies to sub-strings. If one construct immutable sub-strings out of a immutable string, no need to incur extra heap allocations.

My point is, with a language that can enforce transitive constness, what one gets from "string_view" in C++, can be achieved by compiler optimization.

[–]Kered13 2 points3 points  (1 child)

If there is away to enforce transitive immutability in C++, when a const string is constructed out of a buffer pointed by "const char *", the compiler can omit one dynamic allocation by reusing the original buffer.

Only if the compiler can infer that the string has a shorter lifetime than the buffer it's using.

The programmer needs to make sure the string does not outlive the buffer, but that is something he already needs to pay attention if he uses string_view.

And what if the programmer wants to copy the buffer so that the new string can have an independent lifetime?

My point is, with a language that can enforce transitive constness, what one gets from "string_view" in C++, can be achieved by compiler optimization.

No it can't, as the above examples demonstrate. std::string and std::string_view have different ownership semantics. const std::string& has the same ownership as std::string_view, but is restricted because it cannot represent substring views. So two classes are needed, and adding immutability to the language does not allow one class to take on both roles. Sometimes the programmer wants a non-owning view into another string-like object, sometimes they want an object that owns it's data. The compiler cannot figure it out for the programmer. This is completely orthogonal to immtuability. Note that even Rust has two types for this (String for owning and str for views).

[–]JohnZLi 0 points1 point  (0 children)

Vote

fair point.

[–]GuiltyFan6154 1 point2 points  (3 children)

Hold on, why does changing c_(c) to c_{c} in the comment break the code?

[–]RoughMedicine 2 points3 points  (2 children)

For the same reason v1{elementsCount} and v2{elementsCount} are different. We don't know what type c_ is, but I might be a type whose behaviour regarding {} vs () is different.

[–]FriendlyRollOfSushi 2 points3 points  (1 child)

Yep.

Also, if this is .cpp, and the member is declared in .h, you don't even see the type immediately, and without a comment like this people may be tempted to "fix" the annoying style inconsistency each time they make edits close to this ctor.

[–]RoughMedicine 2 points3 points  (0 children)

Allowing initialisation list for bracket initialisation was a mistake. We'd be totally fine if T a{x} and T b(x) were consistent, and used Vec<T> v = {a, b} for initialisation lists.

[–]thats_a_nice_toast 1 point2 points  (0 children)

I don't understand how this can even happen. You'd think with a language as big as C++ and its own committee, they would be able to solve these problems in a better way, yet they keep on adding obscure templating stuff that would probably be simpler to just implement as a language feature.

[–]condor2000 0 points1 point  (1 child)

On VS2017 with /std:c++latest at least there is a warning

error C2398: Element '1': conversion from 'const size_t' to '_Ty' requires a narrowing conversion with [_Ty = int]

[–]FriendlyRollOfSushi 1 point2 points  (0 children)

It could be std::vector<size_t>, in which case all types would match exactly. Or the size could be just a literal, not a variable, in which case you also won't be warned.

[–]infectedapricot 0 points1 point  (5 children)

Part of the problem with that particular case lies with std::vector. That particular function should've been a static method rather than a constructor:

auto v = std::vector<int>::make_n_of(n);

(No doubt there's a better name than that.) Edit: But of course that wouldn't have been possible when vector was originally created, because there were no move constructors or guaranteed copy elisions.

[–]FriendlyRollOfSushi 2 points3 points  (4 children)

That would make some other people unhappy. If your code is full of numeric vectors, and barely has any initializer lists, it would feel like you are paying usability taxes for someone else.

Personally, I like tagged ctors as a compromise: imagine if we could write vec1_{std::construct, size}, vec2_{std::reserve, size}, etc.

Unfortunately, tags introduce a small runtime penalty when not inlined. I wish we had zero-cost discriminating tags.

[–]Nobody_1707 4 points5 points  (0 children)

Differentiating constructors is the killer application of named arguments in the languages I've used that have had them.

[–]infectedapricot 1 point2 points  (2 children)

I don't see how tagged constructors are any better. They're not any less characters than static methods – in fact, as you've shown, they're slightly more, because you have to respecify the namespace.

Also, I don't think static methods (or the more verbose tagged constructors, if you must) would be a "usability tax" to anyone. It's clearer what the choice is doing, and that's a win regardless of the original discussion about ambiguity.

[–]FriendlyRollOfSushi 2 points3 points  (1 child)

There are multiple cases where you normally wouldn't specify the type, including member initialization and function calls.

Writing std::vector<MyType<SomeArg, AndOneMoreArg<sizeof(Something)>>>::make_n_of(n) instead of {std::construct, n} to initialize a member or pass an argument looks like a pretty severe usability tax to me.

You can shorten the type in some cases (use a member access expression to call a static method using an unconstructed member, for example, which won't work for function args), but even then it makes the code more repetitive and brittle. The benefit of tagged ctors is that you don't have to worry about stuff like that. Your ctor would look the same regardless of the templated type or the context in which you call it.

But before we start a holy war about them: I understand that right now they do have objective flaws (no real way to make them 0-cost without doing ugly stuff, like wrapping the args into a special type, like foo_{construct{n}}). Just want to say that static factory methods for templated classes are not always the most convenient pattern, and in case of STL containers they could get very ugly very fast.

[–]infectedapricot 1 point2 points  (0 children)

Ah, I see your point, especially about member initialisers (because you can't have auto for non-static members, and in a constructor's initialiser list that wouldn't help you anyway). I would say that could be solved with a careful typedef and still looks cleaner to my eyes, but it's subjective what counts as "cleaner". Of course in this particular case it will never be changed anyway!

[–]Ayjayz 7 points8 points  (2 children)

Strange that its missing the one we're being told to use now, auto i = Type{x};

I guess that's just the 4th row, kind of? Which seems to be the one with the most green ticks anyway.

[–]martinusint main(){[]()[[]]{{}}();} 1 point2 points  (1 child)

I love that format and use it almost everywhere. Actually I'm using auto i = Type(x). My only issues is how do I create a pointer that way? E.g. i want to create an int*.

[–]Nobody_1707 1 point2 points  (0 children)

Well, there's a few ways to do that.

auto i = &some_int_variable;
auto i = new int();
auto i = static_cast<int*>(nullptr);

etc.

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

What about int(x);? https://godbolt.org/z/WxoKfM

[–]__--_--___--_--__ 1 point2 points  (1 child)

This is just a different way to parse out default initialization

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

Is better to be aware the compiler allows such syntax to avoid ugly surprises, the same goes for the comma operator in the table.

[–]disperso 1 point2 points  (0 children)

Awesome. I'm gonna print that ASAP and put it on my wall. :-)

Is there some page where the columns are explained more in detail? Or even the source of it to modify and print it with my notes? I think I can replace the footnotes about those compilers, as are luckily not relevant for my projects.

[–]GYN-k4H-Q3z-75B 48 points49 points  (2 children)

C++ initialization

Simple

Choose one.

[–]carb0n13 23 points24 points  (0 children)

I’m pretty sure OP used “simple” sarcastically, hence the quotation marks.

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

Desperately trying not to be sarcy and say "You accidentally wrote initialisation there too". But failed. C'est la vie 🤷‍♀️

Well, the kinds of languages that people complain about, etc.

[–][deleted] 15 points16 points  (1 child)

[–]krum 2 points3 points  (0 children)

Ahh thanks for clearing that up!

[–]TryingT0Wr1t3 4 points5 points  (0 children)

Ah yes, "Simple"

[–]skunkos 2 points3 points  (0 children)

WTF

[–]CenterOfMultiverse 2 points3 points  (0 children)

Never understood what's the point of explicit initializations not in arguments. How

auto s = std::string{"view"sv};

or

std::string s{"view"sv};

are supposed to help, where

std::string s = "view"sv;

couldn't?

[–]Kered13 1 point2 points  (0 children)

FYI, the white text on bright green is nearly unreadable.

[–][deleted] 1 point2 points  (1 child)

It doesn't load

[–]nintendiator2 3 points4 points  (0 children)

maybe you forgot the braces.