std::array<>的初始化

15

请考虑以下代码:

#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 表现相同。

我可以看出为什么第一个初始化失败而第二个和第三个是 OK 的。(例如参见 C++11: Correct std::array initialization?。)

我的问题是:为什么最后一个初始化能够编译通过?


很抱歉,我不明白为什么第一个赋值语句无法编译,你能告诉我更多吗?这很有趣! - Telokis
1
@Ninetainedo - 请查看关联的问题。 - Jeremy
1个回答

28
简短版:以{开头的初始化器会停止花括号省略。在使用{1,2}的第一个例子中是这种情况,但在使用A{1,2}的第三个和第四个例子中则不是。花括号省略会消耗接下来的N个初始化器(N取决于要初始化的聚合),这就是为什么只有N的第一个初始化器不能以{开头。
在我知道的所有C++标准库实现中,std::array是一个包含C风格数组的结构体。也就是说,你有一个包含一个子聚合的聚合,就像…
template<typename T, std::size_t N>
struct array
{
    T __arr[N]; // don't access this directly!
};

当使用花括号初始化列表初始化std::array时,您需要初始化包含的数组的成员。因此,在这些实现中,显式形式为:

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

最外层的大括号是指 std::array 结构体;第二层的大括号是指嵌套的 C 风格数组。


C++ 允许在聚合初始化时省略初始化嵌套聚合时的某些大括号。例如:

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

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

规则如下(使用N4527草案,该草案是C++14之后的版本,但C++11中已经存在与此相关的缺陷):
花括号可以在初始化列表中省略。如果初始化列表以左花括号开头,则随后逗号分隔的初始化子句列表将初始化子聚合体的成员;如果初始化子句多于成员,则会出错。然而,如果子聚合体的初始化列表不以左花括号开头,则只有足够的初始化子句被用来初始化子聚合体的成员;任何剩余的初始化子句都将留给当前子聚合体是其成员的聚合体的下一个成员进行初始化。
将此应用于第一个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结构的初始化器。然后,initializer-clauses{1,2},{3,4}等被视为std::array子聚合的初始值设定项。请注意,std::array仅有一个子聚合__arr。由于第一个initializer-clause {1,2}{开头,因此不会发生brace-elision exception,编译器尝试使用{1,2}来初始化嵌套的A __arr [4]数组。剩余的initializer-clauses{3,4},{5,6}等不涉及std::array的任何子聚合,因此是非法的。

在第三个和第四个示例中,std::array子聚合的第一个initializer-clause 不以{开头,因此应用了大括号省略例外。

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

花括号省略规则会消耗接下来的N个初始化子句,其中N是通过需要进行初始化的(子)聚合体所需的初始化器数量来定义的。因此,它只关注接下来的N个初始化子句中的第一个是否以{起始。

更类似于原帖:

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应用花括号省略,并使用两个初始化程序12


很好,回答详细全面。谢谢! - Jeremy
++投票,像往常一样出色的回答。不过有一个问题:已经有N4527草案的后续版本了吗? - Columbo
@Columbo 嗯,不是官方的。这是来自 Github 存储库,因此不是官方工作草案,而是“临时工作材料”。 - dyp
@dyp 我确实查看了Github仓库,但只有N4527版本(我已经使用了一段时间)。 - Columbo
@Columbo N4527是从2015年5月22日开始的,而git存储库包含该日期之后的提交。因此,从源代码构建,您将获得某种N4527之后的“草稿”。 - dyp

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接