Initialisation of std::array<>
Asked Answered
C

1

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?

Cranage answered 16/7, 2015 at 9:6 Comment(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 !Detachment
@Ninetainedo - see the linked question.Cranage
S
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.

Stulin answered 16/7, 2015 at 9:48 Comment(5)
Nice, comprehensive answer. Thanks!Cranage
++vote, excellent answer as usual. One question though: There's a post-N4527 draft already?Delicacy
@Delicacy Well, not officially. It's from the github repo, which makes it not an official working draft, but interim work material.Stulin
@Stulin I did check out the Github repo, but there's N4527 only (which I've been using for a while).Delicacy
@Delicacy 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".Stulin

© 2022 - 2024 — McMap. All rights reserved.