When can outer braces be omitted in an initializer list?
Asked Answered
S

1

56

I've got error C2078 in VC2010 when compiling the code below.

struct A
  {
  int foo;
  double bar;
  };

std::array<A, 2> a1 = 
  // error C2078: too many initializers
  {
    {0, 0.1},
    {2, 3.4}
  };

// OK
std::array<double, 2> a2 = {0.1, 2.3};

I found out that the correct syntax for a1 is

std::array<A, 2> a1 = 
  {{
    {0, 0.1},
    {2, 3.4}
  }};

The question is: why extra braces are required for a1 but not required for a2?

Update

The question seems to be not specific to std::array. Some examples:

struct B
  {
  int foo[2];
  };

// OK
B meow1 = {1,2};
B bark1 = {{1,2}};

struct C
  {
  struct 
    { 
    int a, b; 
    } foo;
  };

// OK
C meow2 = {1,2};
C bark2 = {{1,2}};

struct D
  {
  struct 
    { 
    int a, b; 
    } foo[2];
  };

D meow3 = {{1,2},{3,4}};  // error C2078: too many initializers
D bark3 = {{{1,2},{3,4}}};

I still don't see why struct D gives the error but B and C don't.

Santamaria answered 31/7, 2012 at 7:6 Comment(12)
@KerrekSB: That will not work! See my answer to know the reason.Howard
@Nawaz: Then why does it work for double? Aren't you allowed to collapse braces under certain conditions? Or maybe that was only true for multi-dimensional arrays...Hangman
@KerrekSB: 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 aggregate!Howard
@Nawaz: But note that the OP can do the double array with one pair of braces, as opposed to three for the A type. We're still one pair short!Hangman
@KerrekSB: I think that extra braces is still needed in case of double also, to be completely the Standard conformant, but it works without it. It seems I need to dig the spec again!Howard
@Nawaz: A fully standard-conformant syntax for a2 is std::array<double, 2> a2 = {{0.1, 2.3}};, but I wonder too why it's allowed to omit one pair of braces.Santamaria
@Andrey: That is what I said. +1Howard
Just as a speculation, std::array might have an operator= that takes an std::initializer_list?Hemimorphite
@fish: certainly not the case in VC2010Santamaria
@fish no, std::array is an aggregate, so it can have no such constructor. The problem is that it may be implemented as having a single array as element, which is why the extra braces are required.Wigfall
There is an interesting, related post here.Wigfall
@Andrey: See the edit/quote in my answer.Howard
H
70

The extra braces are needed because std::array is an aggregate and Plain Old Datatype (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.

Howard answered 31/7, 2012 at 7:18 Comment(4)
An interesting, related post here. To me it is not clear at all that the array has to be implemented as a single-element aggregate. The standard provides an example implementation to emphasize that it is an aggregate, but I see no requirement that it be implemented so (although I cannot currently think of an alternative implementation).Wigfall
Great answer! Just found more compilable initializers for struct D from the question: D d1 = {1,2,3,4}; D d2 = {{1,2,3,4}};Santamaria
For those not clicking the link @Wigfall posted, there is link to an official defect report about the brace elision limitation.Hemimorphite
It is not necessary for a type to be a POD for brace elision to apply. Furthermore, something like std::array<std::string> is plainly not a POD.Wisteria

© 2022 - 2024 — McMap. All rights reserved.