15

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?

2
  • I am sorry but I can't see why the first assignment fails to compile, could you tell me more, please ? It's interesting ! Commented Jul 16, 2015 at 9:15
  • 1
    @Ninetainedo - see the linked question. Commented Jul 16, 2015 at 9:20

1 Answer 1

28

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.

Sign up to request clarification or add additional context in comments.

5 Comments

Nice, comprehensive answer. Thanks!
++vote, excellent answer as usual. One question though: There's a post-N4527 draft already?
@Columbo Well, not officially. It's from the github repo, which makes it not an official working draft, but interim work material.
@dyp I did check out the Github repo, but there's N4527 only (which I've been using for a while).
@Columbo N4527 is from 2015-05-22, and the git repo contains commits after that date. So build from source, and you get some kind of post-N4527 "draft".

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.