Инициализация std:: array <>

Рассмотрим следующий код:

#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 }
};

Компиляция с помощью gcc:

$ gcc -c --std=c++11 array.cpp
array.cpp:15:1: error: too many initializers for ‘std::array<A, 4ul>’
 };
 ^
$

NB1: комментируя первый оператор инициализации, код компилируется без ошибок.
NB2: Преобразование всех инициализаций в вызовы конструктора дает одинаковые результаты.
NB3: MSVC2015 ведет себя одинаково.

Я вижу, почему первая инициализация не скомпилируется, и почему вторая и третья в порядке. (например, см. С++ 11: Исправить инициализацию std:: array?.)

Мой вопрос: зачем именно компилируется окончательная инициализация?

Ответы

Ответ 1

Краткая версия: Параметр initializer, начинающийся с {, останавливает скобки. Это имеет место в первом примере с {1,2}, но не в третьем и четвертом, которые используют A{1,2}. Элемент brace-elision потребляет следующие N аргументов инициализатора (где N зависит от инициализации агрегата), поэтому только первое предложение инициализатора N не должно начинаться с {.


Во всех реализациях стандартной библиотеки С++, о которой я знаю, std::array - это структура, содержащая массив C-style. То есть, у вас есть совокупность, которая содержит суб-агрегат, очень похожий на

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

При инициализации std::array из списка с привязкой к init, вам придется инициализировать элементы содержащегося массива. Поэтому на этих реализациях явный вид:

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

Самый внешний набор фигурных скобок относится к структуре std::array; второй набор фигурных скобок относится к вложенному массиву C-стиля.


С++ позволяет в агрегатной инициализации опустить некоторые фигурные скобки при инициализации вложенных агрегатов. Например:

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

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

Правила заключаются в следующем (с использованием проекта post-N4527, который является пост-С++ 14, но С++ 11 содержит дефект, связанный с этим в любом случае):

Брекеты можно отбросить в списке инициализаторов следующим образом. Если Список инициализации начинается с левой скобки, затем следующая список инициализаторов-разделов, разделенных запятыми, инициализирует члены субагрегат; ошибочно, чтобы было больше initializer-clauses, чем члены. Если, однако, список инициализаторов для суммирования не начинается с левой скобки, тогда только достаточно инициализатор-предложения из списка, чтобы инициализировать члены подгруппы; любые оставшиеся условия инициализации слева, чтобы инициализировать следующий член совокупности, из которого текущий субагрегат является членом.

Применив это к первому std::array -примеру:

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

Это интерпретируется следующим образом:

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

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

Первый { берется за инициализатор структуры std::array. Инициализаторы-клаузулы {1,2}, {3,4} и т.д. Затем берутся как инициализаторы субагрегатов std::array. Обратите внимание, что std::array имеет только один элемент subggregate __arr. Так как первое предложение-инициализатор {1,2} начинается с {, исключение из-под бреши не возникает, и компилятор пытается инициализировать вложенный массив A __arr[4] с помощью {1,2}. Оставшиеся пункты инициализации {3,4}, {5,6} и т.д. Не относятся к какой-либо подгруппе std::array и поэтому являются незаконными.

В третьем и четвертом примерах первое предложение-инициализатор для суммирования std::array не начинается с {, поэтому применяется исключение исключения фигурных скобок:

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

Поэтому он интерпретируется следующим образом:

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
  };            // }

Следовательно, A{1,2} приводит к тому, что все четыре условия инициализации потребляются для инициализации вложенного массива C-стиля. Если вы добавите еще один инициализатор:

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

то этот X будет использоваться для инициализации следующего субагрегата std::array. Например.

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

    inner i;
    int c;
};

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

Элемент brace-elision потребляет следующие N аргументов инициализатора, где N определяется с помощью количества инициализаторов, необходимых для инициализации (под). Следовательно, это имеет значение только в том случае, если первый из тех N-инициализаторов-предложений начинается с {.

Аналогично 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
  };             // }

Обратите внимание, что привязка фигуры применяется рекурсивно; мы можем даже написать запутанную

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

Где мы опускаем скобки для o.m и o.m.i. Первые два предложения инициализатора потребляются для инициализации o.m.i, а остальные инициализируются o.c. Как только мы вставим пару фигурных скобок вокруг 1,2, его интерпретируют как пару фигурных скобок, соответствующих o.m:

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

Здесь инициализатор для o.m начинается с символа {, поэтому не применяется. Инициализатором для o.m.i является 1, который не начинается с {, поэтому для o.m.i применяется скобка-elision, и используются два инициализатора 1 и 2.