【问题标题】:Initialisation of std::array<>std::array<> 的初始化
【发布时间】:2015-10-05 15:28:08
【问题描述】:

考虑以下代码:

#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 的行为相同。

我可以看到为什么第一次初始化无法编译,为什么第二次和第三次都可以。 (例如,见C++11: Correct std::array initialization?。)

我的问题是:为什么最终的初始化会编译?

【问题讨论】:

  • 很抱歉,我不明白为什么第一个作业无法编译,您能告诉我更多吗?这很有趣!
  • @Ninetainedo - 查看链接的问题。

标签: c++ c++11 stdarray aggregate-initialization


【解决方案1】:

短版:以{ 开头的初始化子句停止大括号省略。在第一个使用{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!
};

当从 braced-init-list 初始化 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无论如何包含与此相关的缺陷):

可以在 initializer-list 中省略大括号,如下所示。如果 initializer-list 以左大括号开头,然后是后面的 initializer-clauses 的逗号分隔列表初始化 子聚合;有更多是错误的 initializer-clauses 比成员。但是,如果 initializer-list 对于子聚合不以左大括号开头,则仅 从列表中获取足够的 initializer-clauses 来初始化 子集合的成员;任何剩余的 initializer-clauses 都是 left 来初始化聚合的下一个成员,其中 当前子聚合是成员。

将此应用于第一个std::array-example:

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 异常 不会发生,编译器会尝试初始化嵌套的 A __arr[4]带有{1,2} 的数组。剩余的 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} 导致所有四个 initializer-clauses 都被用于初始化嵌套的 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 个初始化子句中的第一个是否以 { 开头才重要。

更类似于 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 应用大括号省略,并且消耗两个初始值设定项1 和2。

【讨论】:

  • 不错,全面的答案。谢谢!
  • ++投票,一如既往的出色回答。不过有一个问题:N4527 之后的草案已经有了?
  • @Columbo 好吧,不是正式的。它来自 github repo,因此它不是正式的工作草案,而是 临时工作材料。
  • @dyp 我确实查看了 Github 存储库,但只有 N4527(我已经使用了一段时间)。
  • @Columbo N4527 来自 2015-05-22,并且 git repo 包含该日期之后的提交。所以从源代码构建,你会得到某种 N4527 后的“草稿”。
猜你喜欢
  • 1970-01-01
  • 2012-02-10
  • 2021-03-19
  • 1970-01-01
  • 2023-02-06
  • 1970-01-01
  • 1970-01-01
  • 2021-06-26
  • 2017-04-17
相关资源
最近更新 更多