Consider the following code:
#include <array>
struct A
{
int a;
int b;
};
static std::array<A, 4> x1 =
{
{ 1, 2 },
{ 3, 4 },
{ 5, 6 },
{ 7, 8 }
};
static std::array<A, 4> x2 =
{
{
{ 1, 2 },
{ 3, 4 },
{ 5, 6 },
{ 7, 8 }
}
};
static std::array<A, 4> x3 =
{
A{ 1, 2 },
A{ 3, 4 },
A{ 5, 6 },
A{ 7, 8 }
};
static std::array<A, 4> x4 =
{
A{ 1, 2 },
{ 3, 4 },
{ 5, 6 },
{ 7, 8 }
};
Compiling with gcc:
$ gcc -c --std=c++11 array.cpp
array.cpp:15:1: error: too many initializers for ‘std::array<A, 4ul>’
};
^
$
NB1: Commenting out the first initialisation statement, the code compiles without error.
NB2: Converting all the initialisation to constructor calls yields the same results.
NB3: MSVC2015 behaves the same.
I can see why the first initialisation fails to compile, and why the second and third are OK. (e.g. See C++11: Correct std::array initialization?.)
My question is: Why exactly does the final initialisation compile?
std::array contains a built-in array, which can be initialized via an initializer list, which is what the inner set is.
std::array::array default-initialization: Each of the elements is itself default-initialized. For elements of a class type this means that their default constructor is called.
Initializer List: To initialize an array in C with the same value, the naive way is to provide an initializer list. We use this with small arrays. int num[5] = {1, 1, 1, 1, 1}; This will initialize the num array with value 1 at all index.
For char arrays, the default value is '\0' . For an array of pointers, the default value is nullptr . For strings, the default value is an empty string "" . That's all about declaring and initializing arrays in C/C++.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With