13

Uniform initialization is an important and useful C++11 feature. However, you can't just use {} everywhere since:

std::vector<int> a(10, 0);    // 10 elements of value zero
std::vector<int> b({10, 0});  // 2 elements of value 10 and 0 respectively
std::vector<int> c{10, 0};    // 2 elements of value 10 and 0 respectively
std::vector<int> d = {10, 0}; // 2 elements of value 10 and 0 respectively

auto e(0);    // deduced type is int
auto f = 0;   // deduced type is int
auto g{0};    // deduced type is std::initializer_list<int>
auto h = {0}; // deduced type is std::initializer_list<int>

Noting that aggregate initialization on e.g. std::arrays requires the use of {{}}, it seems to me that the whole problem with which vector constructor will be selected could have been avoided by requiring a {{}} to call constructors taking a std::initializer_list:

std::vector<int> i{10, 0};    // 10 elements of value zero
std::vector<int> j{{10, 0}};  // 2 elements of value 10 and 0 respectively
std::vector<int> k = {10, 0}; // 2 elements of value 10 and 0 respectively

auto l{0};    // deduced type is int
auto m{{0}};  // deduced type is std::initializer_list<int>
auto n = {0}; // deduced type is std::initializer_list<int>

I'm sure this was discussed, so what were the reasons against this? A quote/link from a standard proposal is preferred as answer.

Update. — There is a point in N2532 that states:

(3) The likely nasty ambiguity cases occur only for short initializer lists [...]

(5) Why should the language rules force programmers who wants terseness and ambiguity control (for perfectly good reasons) to write more to please programmers who prefer (for perfectly good reasons) to be more explicit – and can be?

[...]

Assume that a programmer expects f(X) to be called. How might a f(Y) “hijack” a call?

(4) Assume that X has no initializer-list constructor, but Y does. In this case, the priority given to initializer-list constructors favor the hijacker (remember we assumed that the programmer somehow expected f(X) to be called). This is analogous to someone expecting f(y) to invoke f(X) using a user-defined conversion and someone comes along with an f(Y) that matches exactly. I think it would be fair to expect that someone who uses {…} will remember the possibility of initializer-lists constructors. [emphasis mine]

I guess the key lies in the can be, which means you don't have to use uniform initialization. Using {} correctly is hard since:

  • you not only have to check for the constructor you want to call but also for any constructor taking an initializer_list that might win (and probably will) over it;

  • if you write code using {} and someone in the future adds an std::initializer_list constructor your code might break and do so silently.

Even if you have a class A with the constructors A(int, bool) and A(std::initializer_list<double>), the latter will be selected over the former for A a{0, false}; (which IMO is nuts), so I find it really hard to use uniform initialization on classes that have or might have (crystal ball superpowers required) initializer_list constructors.

The fact that your code can silently break worries me a lot.

Géry Ogam
  • 6,336
  • 4
  • 38
  • 67
gnzlbg
  • 7,135
  • 5
  • 53
  • 106
  • I'm not sure which "problem" you are trying to solve. You are just showing a different way of doing things. The only "problem" I see is `std::array` possibly requiring two sets of braces. But I think this is being fixed. – juanchopanza Mar 19 '14 at 09:38
  • 2
    The problem I'm try to solve is finding out why this _different way_ of doing things wasn't standarized (and the current one was). In particular, I'll probably need the reason behind this decision or good counter-examples against it to be satisfied. – gnzlbg Mar 19 '14 at 09:42
  • I don't think anybody would want `auto e{0, 0};` to be an error. Nor do I think anybody would want it to be treated as the application of the comma operator. The only sensible type for that is `std::initializer_list` (or something like `int[]`). But if `auto e{(list of ints)};` is to be resolved as `std::initializer_list`, and `auto e{0};` is in the form `auto e{(list of ints)};`, then it would cause special unnecessary complications to add specific exceptions based on the list's length. –  Mar 19 '14 at 09:50
  • 1
    As for your `auto d = {0}; // type of d is int` -- either that's wrong, or GCC's implementation is. GCC also makes `d` of type `std::initializer_list`. –  Mar 19 '14 at 09:53
  • @hvd yes that is wrong, the type is `std::initializer_list! Sorry about that! I hope it makes sense now! – gnzlbg Mar 19 '14 at 09:55
  • @hvd I would prefer `auto e{0,0}` to be the application of the comma operator, and `auto e{{0,0}}` to deduce to an `initializer_list`. Just for consistency, since `decltype(0,0)` deduces to an application of the comma operator: `decltype(0,0) a = 0` and `auto a{0,0}; a = 0;` are not currently even remotely similar. – gnzlbg Mar 19 '14 at 09:58
  • 1
    You are proposing something with semantics that look very confusing. So you expect to find some document somewhere explaining why `auto a{0,0}` does not use the comma operator? Maybe they have it in [The Library of Babel](http://en.wikipedia.org/wiki/The_Library_of_Babel). – juanchopanza Mar 19 '14 at 10:06
  • @juanchopanza are you suggesting that the semantics of the standarized solution are not confusing? I'm proposing something consistent, and I've found a document where it is partially discussed albeit not fully. I'm furthermore pretty sure that there are more documents like it as well as threads in std.proposals mailing list that discuss this issue, I'm just not able to find them. Moreover, I know that it wasn't done like i'm proposing for a good reason. – gnzlbg Mar 19 '14 at 10:11
  • @gnzlbg So you would want the comma in `auto e = {0,0};` to be parsed as an operator, even though it is not in `int e[] = {0,0};`, even before `auto` is resolved to a concrete type? The latter cannot change, that would be an enormous backwards incompatibility. –  Mar 19 '14 at 10:16
  • @hvd `auto e = {0,0}` is unambiguously an initializer_list, the `auto e{...}` is IMO inconsistent with `decltype`. Those two cases are IMO uncommon. However, in the context of constructors taking few arguments vs overloads taking initializer_list, the unexpected might happen silently (and IMO does happen). I've put some emphasis on the question that the problem lies in which vector constructor is selected. – gnzlbg Mar 19 '14 at 10:20
  • Ah, okay, I didn't realise you'd want `auto e{...}` and `auto e = {...}` to deduce differently. I think that that would also cause a lot of confusion, though. –  Mar 19 '14 at 10:37
  • 2
    maybe the confusion comes from using `{}` to denote both uniform initialization _and_ `std::initializer_lists`. – gnzlbg Mar 19 '14 at 10:38
  • @hvd, confusing maybe, but in C++14 `auto e{...}` and `auto e = {...}` do deduce differently, see [N3922](http://open-std.org/jtc1/sc22/wg21/docs/papers/2014/n3922.html) which was voted into the working paper last month. – Jonathan Wakely Mar 19 '14 at 12:42
  • 1
    @gnzlbg, _"`decltype(0,0) a = 0` and `auto a{0,0}; a = 0;` are not currently even remotely similar"_ ... and that's a good thing. `decltype` tells you the type of an expression, an initializer-list is a _list_ of expressions, not a single expression. Commas in lists do not use the comma operator (consider function parameter lists, template argument lists, ctor-initializer-lists etc. etc.) – Jonathan Wakely Mar 19 '14 at 12:47
  • 1
    @JonathanWakely thanks for the link to N3922! I'm not convinced myself about decltype/auto in this case so I'd rather focus the discussion on the problems with constructor overloads taking an initializer_list which is what motivated me to ask the question. IMO `X a{...};` constructs an `X` passing `...` to its constructor, so the obvious would be to use `X a{ {...} }` to pass a single initializer list (`X a{ {...}, {...} }` to pass two initializer lists,...). The shortcut `X a{...}` where `...` gets deduced to be an initializer list seems to give more headaches than it is worth. – gnzlbg Mar 19 '14 at 13:40
  • @JonathanWakely Thanks, and wow. Exactly what I couldn't imagine anyone would want an error for, is in that as the example of what will become an error. :) –  Mar 19 '14 at 13:59
  • @gnzlbg: Thanks a lot for this post, I totally agree with your suggestion. Currently the situation is a mess as Scott Meyers reported in his [blog](http://scottmeyers.blogspot.fr/2014/03/if-braced-initializers-have-no-type-why.html). – Géry Ogam Jul 31 '15 at 14:38

2 Answers2

13

Here's what Stroustrup has said on the subject:

Uniform and universal was not designed to be just a 4th alternative. It was designed to be the initialization syntax,and was unfortunately [not] feasible to use with all legacy code, especially vector. Had I designed vector today, you would have had to say something like vector<int> {Count{9}}; to get a count.

And in response to the question "Is the problem vector or {}-init syntax?"

It's the vector design: Had I designed vector today, you would have had to say something like vector<int> {Count{9}}; to get a count.

The more general problem is to have several semantically different arguments of the same type eventually leads to confusion, especially if they can appear adjectly. For example:

vector<int> v(7,2);    // 7 (a count) element with the value 2
Community
  • 1
  • 1
Jonathan Wakely
  • 166,810
  • 27
  • 341
  • 521
6

(This isn't really an answer, just a discussion of what I've thought about on this issue.)

I think I'd like the compilers to give a warning in cases where there is ambiguity, advising the developer to use either ({ }) (if they do want the initializer_list), or ( ) if they do not. With an extra warning if the MostVexingParse is a risk! - perhaps recommend (( )) to avoid that?

(This following 'story' is probably not based on the correct historical chronology of how the feature was developed, but it's how I understand the current rules in the compiler.)

In the beginning we had constructors:

type t (...);

Then we had the idea of allowing { to give a literal collection for use in constructors (but also elsewhere).

type t ( {...} );

... along with a new initializer_list type in the constructors to match this.

Then, we were allowed to replace ( ) with { } in order to avoid the Most Vexing Parse:

type t { ... };
type t { {...} };

So far, so good. Pure extensions to the language.

Finally, the 'controversial' addition is that, when the compiler sees { ... } (as a constructor) it will first attempt to rewrite that as ({ ... }) (calling the initializer_list if it exists) before falling back on ( ... ). I think I'd prefer if both options were considered equally good and there to be a warning or error if both are possible.

Aaron McDaid
  • 26,501
  • 9
  • 66
  • 88