Brace Elision in Std::Array Initialization

Brace elision in std::array initialization

Brace elision applies, but not in C++11. In C++14, they will apply because of http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_defects.html#1270 . If you are lucky, Clang will backport that to their C++11 mode (let's hope they will!).

Double braces required for list-initialization of container std::array

Braces, nested however deeply, are matched against the structure of the object being initialized before considering any type information. Since std::array<T,N> must contain a true T[N] (rather than be one), the structure is that there is one object inside the array—that is, the array. Two opening braces are therefore taken to begin the initializer for that array, which doesn’t work if the entire nested set is needed to initialize one array element, nor if there is more than one such nested set.

When some initializer clause is an expression, even A{…}, this decomposition stops and the initializer is used for one subobject. However, if that expression cannot be converted to the type of the appropriate subobject and that type is itself an aggregate, brace elision takes place and the subsequent initializers are used for its subobjects despite the lack of braces. When that applies to the T[N] object itself, the array may be initialized with only one layer of braces.

So, in short, an open brace causes decomposition of an aggregate, whether it works or not, but a type mismatch also causes decomposition of an aggregate.

C++11: Correct std::array initialization?

This is the bare implementation of std::array:

template<typename T, std::size_t N>
struct array {
T __array_impl[N];
};

It's an aggregate struct whose only data member is a traditional array, such that the inner {} is used to initialize the inner array.

Brace elision is allowed in certain cases with aggregate initialization (but usually not recommended) and so only one brace can be used in this case. See here: C++ vector of arrays

Initialisation of std::array

Short version: An initializer-clause that starts with { stops brace-elision. This is the case in the first example with {1,2}, but not in the third nor fourth which use A{1,2}. Brace-elision consumes the next N initializer-clauses (where N is dependent on the aggregate to be initialized), which is why only the first initializer-clause of the N must not begin with {.


In all implementations of the C++ Standard Library I know of, std::array is a struct which contains a C-style array. That is, you have an aggregate which contains a sub-aggregate, much like

template<typename T, std::size_t N>
struct array
{
T __arr[N]; // don't access this directly!
};

When initializing a std::array from a braced-init-list, you'll therefore have to initialize the members of the contained array. Therefore, on those implementations, the explicit form is:

std::array<A, 4> x = {{ {1,2}, {3,4}, {5,6}, {7,8} }};

The outermost set of braces refers to the std::array struct; the second set of braces refers to the nested C-style array.


C++ allows in aggregate initialization to omit certain braces when initializing nested aggregates. For example:

struct outer {
struct inner {
int i;
};
inner x;
};

outer e = { { 42 } }; // explicit braces
outer o = { 42 }; // with brace-elision

The rules are as follows (using a post-N4527 draft, which is post-C++14, but C++11 contained a defect related to this anyway):

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 members of
a subaggregate; it is erroneous for there to be more
initializer-clauses than members. 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
members of the subaggregate; any remaining initializer-clauses are
left to initialize the next member of the aggregate of which the
current subaggregate is a member.

Applying this to the first std::array-example:

static std::array<A, 4> x1 =
{
{ 1, 2 },
{ 3, 4 },
{ 5, 6 },
{ 7, 8 }
};

This is interpreted as follows:

static std::array<A, 4> x1 =
{ // x1 {
{ // __arr {
1, // __arr[0]
2 // __arr[1]
// __arr[2] = {}
// __arr[3] = {}
} // }

{3,4}, // ??
{5,6}, // ??
...
}; // }

The first { is taken as the initializer of the std::array struct. The initializer-clauses {1,2}, {3,4} etc. then are taken as the initializers of the subaggregates of std::array. Note that std::array only has a single subaggregate __arr. Since the first initializer-clause {1,2} begins with a {, the brace-elision exception does not occur, and the compiler tries to initialize the nested A __arr[4] array with {1,2}. The remaining initializer-clauses {3,4}, {5,6} etc. do not refer to any subaggregate of std::array and are therefore illegal.

In the third and fourth example, the first initializer-clause for the subaggregate of std::array does not begin with a {, therefore the brace elision exception is applied:

static std::array<A, 4> x4 =
{
A{ 1, 2 }, // does not begin with {
{ 3, 4 },
{ 5, 6 },
{ 7, 8 }
};

So it is interpreted as follows:

static std::array<A, 4> x4 =
{ // x4 {
// __arr { -- brace elided
A{ 1, 2 }, // __arr[0]
{ 3, 4 }, // __arr[1]
{ 5, 6 }, // __arr[2]
{ 7, 8 } // __arr[3]
// } -- brace elided
}; // }

Hence, the A{1,2} causes all four initializer-clauses to be consumed to initialize the nested C-style array. If you add another initializer:

static std::array<A, 4> x4 =
{
A{ 1, 2 }, // does not begin with {
{ 3, 4 },
{ 5, 6 },
{ 7, 8 },
X
};

then this X would be used to initialize the next subaggregate of std::array. E.g.

struct outer {
struct inner {
int a;
int b;
};

inner i;
int c;
};

outer o =
{ // o {
// i {
1, // a
2, // b
// }
3 // c
}; // }

Brace-elision consumes the next N initializer-clauses, where N is defined via the number of initializers required for the (sub)aggregate to be initialized. Therefore, it only matters whether or not the first of those N initializer-clauses starts with a {.

More similarly to the OP:

struct inner {
int a;
int b;
};

struct outer {
struct middle {
inner i;
};

middle m;
int c;
};

outer o =
{ // o {
// m {
inner{1,2}, // i
// }
3 // c
}; // }

Note that brace-elision applies recursively; we can even write the confusing

outer o =
{ // o {
// m {
// i {
1, // a
2, // b
// }
// }
3 // c
}; // }

Where we omit both the braces for o.m and o.m.i. The first two initializer-clauses are consumed to initialize o.m.i, the remaining one initializes o.c. Once we insert a pair of braces around 1,2, it is interpreted as the pair of braces corresponding to o.m:

outer o =
{ // o {
{ // m {
// i {
1, // a
2, // b
// }
} // }
3 // c
}; // }

Here, the initializer for o.m does start with a {, hence brace-elision does not apply. The initializer for o.m.i is 1, which does not start with a {, hence brace-elision is applied for o.m.i and the two initializers 1 and 2 are consumed.

Why does initialization of array of pairs still need double braces in C++14?

This appears to be a parsing ambuguity somewhat similar to the famous most vexing parse. I suspect what's going on is that:

If you write

std::array<std::pair<int, int>, 3> b {{1, 11}, {2, 22}, {3, 33}};

the compiler has two ways to interpret the syntax:

  1. You perform a full-brace initialization (meaning the outermost brace refers to the aggregate initialization of the std::array, while the first innermost one initializes the internal member representation of std::array which is a real C-Array). This will fail to compile, as std::pair<int, int> subsequently cannot be initialized by 1 (all braces are used up). clang will give a compiler error indicating exactly that:

    error: no viable conversion from 'int' to 'std::pair<int, int>'
    std::array<std::pair<int, int>, 3> a{{1, 11}, {2, 22}, {3, 33}};
    ^

    Note also this problem is resolved if there is no internal member aggregate to be initialized, i.e.

    std::pair<int, int> b[3] = {{1, 11}, {2, 22}, {3, 33}};

    will compile just fine as aggregate initialization.

  2. (The way you meant it.) You perform a brace-elided initialization, the innermost braces therefore are for aggregate-initialization of the individual pairs, while the braces for the internal array representations are elided. Note that even if there wasn't this ambiguity, as correctly pointed out in rustyx's answer, the rules of brace elision do not apply as std::pair is no aggregate type so the program would still be ill-formed.

The compiler will prefer option 1. By providing the extra braces, you perform the full-brace initialization and lift any syntactical ambiguity.

Why can't a 2D std::array be initialized with two layers of list-initializers?

The container std::array is equivalently a struct holding a C-array (an implementation may not implement std::array in this way, but it should guarantee the semantic is the same), so it should be initialized by two layers of braces, i.e.

#include <array>
std::array<std::array<double,2>,2> f() {
return {{{{0,0}},{{0,0}}}};
}

Of course, braces in an initializer-list can be elided like what we usually do for a 2D array:

int arr[2][2] = {0,1,2,3};

... but the initializer-list that begins with the elided braces before the elision should not begin with a left brace after the elision. In other words, if an initializer-list begins with a left brace, the compiler will not consider the possibility that this initializer-list has elided outermost braces.

In your initializer {{0,0},{0,0}}, the sub-initializer {0,0},{0,0} begins with a left brace, so it is used to initialize the C-array itself. However, there are two clauses in the list while there is only one C-array, an error occurs.

In your initializer {std::array<double,2>{0,0},{0,0}}, the sub-initializer std::array<double,2>{0,0},{0,0} does not begin with a left brace, so it can be used to initialize the elements of the C-array, which is OK (recursively, {0,0} is OK to initialize an std::array<double,2> because the sub-initializer 0,0 does not begin with a left brace).


A suggestion: with this elision rule of braces, you can elide all inner braces, just like what we usually do for a 2D array:

#include <array>
std::array<std::array<double,2>,2> f() {
return {0,0,0,0};
}

When can outer braces be omitted in an initializer list?

The extra braces are needed because std::array is an aggregate and POD, unlike other containers in the standard library. std::array doesn't have a user-defined constructor. Its first data member is an array of size N (which you pass as a template argument), and this member is directly initialized with an initializer. The extra braces are needed for the internal array which is being directly initialized.

The situation is same as:

//define this aggregate - no user-defined constructor
struct Aarray
{
A data[2]; //data is an internal array
};

How would you initialize this? If you do this:

Aarray a1 =
{
{0, 0.1},
{2, 3.4}
};

it gives a compilation error:

error: too many initializers for 'Aarray'

This is the same error which you get in the case of a std::array (if you use GCC).

So the correct thing to do is to use braces as follows:

Aarray a1 =
{
{ //<--this tells the compiler that initialization of `data` starts

{ //<-- initialization of `data[0]` starts

0, 0.1

}, //<-- initialization of `data[0]` ends

{2, 3.4} //initialization of data[1] starts and ends, as above

} //<--this tells the compiler that initialization of `data` ends
};

which compiles fine. Once again, the extra braces are needed because you're initializing the internal array.

--

Now the question is why are extra braces not needed in case of double?

It is because double is not an aggregate, while A is. In other words, std::array<double, 2> is an aggregate of aggregate, while std::array<A, 2> is an aggregate of aggregate of aggregate1.

1. I think that extra braces are still needed in the case of double also (like this), to be completely conformant to the Standard, but the code works without them. It seems I need to dig through the spec again!.

More on braces and extra braces

I dug through the spec. This section (§8.5.1/11 from C++11) is interesting and applies to this case:

In a declaration of the form

T x = { a };

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 members of a subaggregate; it is erroneous for there to be more initializer-clauses than members. 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 members of the subaggregate; any remaining initializer-clauses are left to initialize the next member of the aggregate of which the current subaggregate is a member. [ Example:

float y[4][3] = {
{ 1, 3, 5 },
{ 2, 4, 6 },
{ 3, 5, 7 },
};

is a completely-braced initialization: 1, 3, and 5 initialize the first row of the array y[0], namely y[0][0], y[0][1], and y[0][2]. Likewise the next two lines initialize y[1] and y[2]. The initializer ends early and therefore y[3]s elements are initialized as if explicitly initialized with an expression of the form float(), that is, are initialized with 0.0. In the following example, braces in the initializer-list are elided; however the initializer-list has the same effect as the completely-braced initializer-list of the above example,

float y[4][3] = {
1, 3, 5, 2, 4, 6, 3, 5, 7
};

The initializer for y begins with a left brace, but the one for y[0] does not, therefore three elements from the list are used. Likewise the next three are taken successively for y[1] and y[2]. —end example ]

Based on what I understood from the above quote, I can say that the following should be allowed:

//OKAY. Braces are completely elided for the inner-aggregate
std::array<A, 2> X =
{
0, 0.1,
2, 3.4
};

//OKAY. Completely-braced initialization
std::array<A, 2> Y =
{{
{0, 0.1},
{2, 3.4}
}};

In the first one, braces for the inner-aggregate are completely elided, while the second has fully-braced initialization. In your case (the case of double), the initialization uses the first approach (braces are completely elided for the inner aggregate).

But this should be disallowed:

//ILL-FORMED : neither braces-elided, nor fully-braced
std::array<A, 2> Z =
{
{0, 0.1},
{2, 3.4}
};

It is neither braces-elided, nor are there enough braces to be completely-braced initialization. Therefore, it is ill-formed.

Value-initializing std::array member?

You can use inline member initialization:

private:
std::array<T, 42> a{};

If you absolutely want to do it with a constructor instead (why though?) then:

C()
: a{}
{ }

List initialisation of two dimensional std::array

In this declaration

std::array<std::array<int,2>,2> x = {{0,1},{2,3}};

you have three nested aggregates. The first pair of values enclosed in braces

{0,1}

is considered by the compiler as an initializer of the second aggregate that is present in the declaration as one sub-aggregate. So the second pair of values in braces

{2,3}

are considered by the compiler as redundant that has no corresponding object.

You could declare the array for example like

std::array<std::array<int, 2>, 2> x = { { {0,1},{2,3} } };

The braces may be elided when an aggregate is initialized. (C++17 Standard, 11.6.1 Aggregates)

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

So in this declaration

std::array<std::array<int,2>,2> x = {0,1,2,3};

the braces are elided and the aggregate is initialized as it is described in the quote..

In this declaration

std::vector<std::vector<int>> y = {{0,1},{2,3}};

there is used the constructor of the class std::vector that accepts std::initializer_list as an argument. In this case the constructor builds as many elements of the vector as there are elements in the initializer list.



Related Topics



Leave a reply



Submit