Brace elision in std::array<std::vector>
Asked Answered
M

2

7

I'm compiling using g++ for C++ 17. I have the following:

std::array<std::vector<int>, 2> v = {{ {1,2}, {3,4} }};

I don't understand why if I remove the double braces for the array it does not work anymore.

std::array<std::vector<int>, 2> v = { {1,2}, {3,4} }; // Does not compile

I understand how std::array works and the need for the double braces in general, but as I'm compiling for C++17 I expected brace elision to come into play.

Why is brace elision not applicable here?

Monkhood answered 14/11, 2018 at 17:32 Comment(6)
What do you mean by "not working" - the code compiles with GCC 8.1.0 and -std=c++17Oriflamme
@NeilButterworth He meant about this. If I understood correctly.Neils
@Neils Yes you are correct.Monkhood
@Neils That code isn't compiled with -std=c++17 Oriflamme
@NeilButterworth g++ -std=c++17 main.cpp main.cpp: In function ‘int main()’: main.cpp:5:56: error: too many initializers for ‘std::array<std::vector<int>, 2>’ std::array<std::vector<int>, 2> v = { {1,2}, {3,4} };Monkhood
Related: Why can't a 2D std::array be initialized with two layers of list-initializers?.Publicness
A
10

std::array<std::vector<int>, 2> is effectively

struct array {
    std::vector<int> elems[2];
};

elems is a subaggregate just fine. The issue is that per the language rules, if the initializer starts with a { it's always assumed that you aren't eliding braces; instead, {1, 2} is taken as the initializer of the entire subaggregate elems, attempting to initialize its first element with 1 and second element with 2 (this is obviously invalid - you can't convert an integer to a vector - but doesn't affect the interpretation), and {3, 4} is considered the initializer for the thing after elems - and since there are no such thing, it's another error.

Initializing the first element with something that's not a braced-init-list is sufficient to trigger brace elision:

std::array<std::vector<int>, 2> v = { std::vector<int>{1,2}, {3,4} }; 

Note that from a specification perspective, the library doesn't guarantee initialization of std::array<T, N> from anything other than another std::array<T, N> or a list of "up to N elements whose types are convertible to T". This notably excludes braced-init-lists because they have no type, and actually also disallows "double braces" because that's just a special case of having a single element that is a braced-init-list .

This is an area where we may have been better off specifying it with code. The core language rules defy easy specification in words and the implementation details will leak out - and have already done so.

Austenite answered 14/11, 2018 at 20:19 Comment(2)
Thank you for the clarification, it was very useful. I'm not sure I fully understood your second remark though. The initialization is not guaranteed in other cases means that it is unspecified behaviour? Why are also double braces disallowed (considering that the brace elision rule was made pretty much for std::array IIUC)?Monkhood
Formally, it's undefined behavior: the standard defines no behavior when a std::array is initialized with anything else. Double braces are not in the "defined to work" case because they are actually an initializer list containing a single element (the inner braced-init-list) with no type. Brace elision goes all the way back to C. std::array has nothing to do with it. The formal UB here is certainly a defect; the problem is finding the right words to define the behavior, preferably without regurgitating the several pages of aggregate initialization rules.Austenite
M
4

As T.C. pointed out my original interpretation was not corret, brace elision is allowed see [dcl.init.aggr]p15:

Braces can be elided in an initializer-list as follows. If the initializer-list begins with a left brace, then the succeeding comma-separated list of initializer-clauses initializes the elements of a subaggregate; it is erroneous for there to be more initializer-clauses than elements. If, however, the initializer-list for a subaggregate does not begin with a left brace, then only enough initializer-clauses from the list are taken to initialize the elements of the subaggregate; any remaining initializer-clauses are left to initialize the next element of the aggregate of which the current subaggregate is an element. ...

but std::array according to array.overview:

An array is an aggregate that can be list-initialized with up to N elements whose types are convertible to T.

which is not the case we have.

Morra answered 14/11, 2018 at 17:52 Comment(7)
But in my example the double brace is only required for std::array, which is an an aggregate, right? Or is brace elision only allowed when it's aggregate all the way down?Monkhood
I just noticed that std::array<std::vector<int>, 2> v = {std::vector<int>{1,2}, std::vector<int>{3,4}}; compiles. Is this also the "does not begin with a left brace" case?Monkhood
@Monkhood that is a different case of explicit initialization covered by dcl.init.aggrp3.2 and dcl.init.aggrp4.2)Morra
OP's example is attempting to elide the braces for the std::vector<int>[2] subaggregate.Austenite
@Austenite so are you saying it should work and the compilers are non-conforming or that my choice of words is not correct or that it should not work but I am quoting the wrong parts?Morra
If the initializer-list begins with a left brace, Does this means if the first initializer-clause of the initializer-list is a brace-init-list?Exponible
This is the right portion of the rules, but you are giving it the wrong interpretation. "subaggregate" doesn't require "aggregate all the way down", and this example definitely relies on brace elision and is not related to what you quoted in response (std::array has exactly one element).Austenite

© 2022 - 2024 — McMap. All rights reserved.