C++11中的数组声明和初始化

11

以下是在g++下看起来没问题的在C++11中声明和初始化数组的8种方式:

/*0*/ std::array<int, 3> arr0({1, 2, 3});
/*1*/ std::array<int, 3> arr1({{1, 2, 3}});
/*2*/ std::array<int, 3> arr2{1, 2, 3};
/*3*/ std::array<int, 3> arr3{{1, 2, 3}};
/*4*/ std::array<int, 3> arr4 = {1, 2, 3};
/*5*/ std::array<int, 3> arr5 = {{1, 2, 3}};
/*6*/ std::array<int, 3> arr6 = std::array<int, 3>({1, 2, 3});
/*7*/ std::array<int, 3> arr7 = std::array<int, 3>({{1, 2, 3}});

按照严格标准(以及即将发布的C++14标准),哪些是正确的?哪些是最常见/常用的,需要避免使用(原因是什么)?


当询问g++时,请使用“-Wall -pedantic”进行编译。对于Clang也是如此,这会有很大帮助。例如,“arr 4”缺少第二组{}。 - usr1234567
2
首先,我想知道为什么 arr0arr1 能够正常工作;它们是否调用了移动构造函数?如果是这样,它们的含义就与 2-5 不同(类似于 6 和 7)。 - dyp
为什么要回滚?版本号很有用,不会分散注意力。 - user837703
1
@dudeprgm,最佳答案说“示例0、2、6不需要工作”。然而,从您的编号中并不清楚哪些行是这些。如果没有您的编辑,我会认为“示例0”表示arr0,因此您的编号增加了混淆(至少对我来说是这样)。 - M.M
1
@MattMcNabb ...抱歉,我回滚到了一个早期版本,其中数字从“示例1”开始,而不是“示例0”。现在应该已经修复了。 - user837703
4个回答

19

C++11摘要 / TL;DR

  • 由于花括号省略缺陷,示例0、2、6不需要工作。然而,最近版本的编译器实现了该缺陷的建议解决方案,因此这些示例将会工作。
  • 由于未指定std::array是否包含原始数组,因此不需要使示例1、3、5、7工作。但是,我不知道有哪个标准库实现它们不工作(在实践中)。
  • 示例4将始终工作:std::array<int, 3> arr4 = {1, 2, 3};

我更喜欢第4版或第2版(带有花括号省略修复),因为它们直接初始化并且需要/可能工作。

对于Sutter的AAA样式,您可以使用auto arrAAA = std::array<int, 3>{1, 2, 3};,但这需要花括号省略修复。


std::array 要求是一个聚合体 [array.overview]/2,这意味着它没有用户提供的构造函数(即只有默认、拷贝和移动构造函数)。


std::array<int, 3> arr0({1, 2, 3});
std::array<int, 3> arr1({{1, 2, 3}});

使用 (..) 进行初始化是直接初始化,这需要调用构造函数。在 arr0arr1 的情况下,只有复制/移动构造函数是可行的。因此,这两个例子意味着从大括号初始化列表创建一个临时的 std::array,并将其复制/移动到目标位置。通过复制/移动省略,即使它具有副作用,编译器也允许省略该复制/移动操作。 请注意,即使临时值是 prvalues,由于 std::array 的移动构造函数可能没有被隐式声明(例如如果它被删除了),因此可能会触发一次复制(在复制省略之前语义上)。
std::array<int, 3> arr6 = std::array<int, 3>({1, 2, 3});
std::array<int, 3> arr7 = std::array<int, 3>({{1, 2, 3}});

以下是复制初始化的示例。创建了两个临时对象:
- 通过花括号初始化列表 {1, 2, 3} 调用复制/移动构造函数 - 通过表达式 std::array<int, 3>(..) 然后将后一个临时对象复制/移动到命名的目标变量。可以省略创建这两个临时对象。
据我所知,实现可能编写一个 explicit array(array const&) = default; 构造函数并且不违反标准;这将使得上述示例无效。(通过 [container.requirements.general] 规则出现了这种可能性,感谢 David Krauss,参见 this discussion。)
std::array<int, 3> arr2{1, 2, 3};
std::array<int, 3> arr3{{1, 2, 3}};
std::array<int, 3> arr4 = {1, 2, 3};
std::array<int, 3> arr5 = {{1, 2, 3}};

这是聚合初始化。它们都“直接”初始化 std::array,而不调用 std::array 的构造函数,也不(语义上)创建临时数组。 std::array 的成员通过复制初始化进行初始化(见下文)。

关于花括号省略的话题:

在C++11标准中,花括号省略只适用于形如T x = { a };的声明,但不适用于T x { a };。这被认为是一个缺陷,并将在C++1y中得到修复,但是提议的解决方案不是标准的一部分(DRWP状态,请参见链接页面顶部),因此您不能指望编译器也为T x { a };实现它。

因此,std::array<int, 3> arr2{1, 2, 3};(示例0、2、6)严格来说是不合法的。据我所知,最近版本的clang++和g++已经允许在T x { a };中使用花括号省略。

在示例6中,std :: array<int,3>({1,2,3})使用复制初始化:参数传递的初始化也是复制初始化。然而,括号省略的缺陷限制“在形式为T x = {a};的声明中”也禁止了参数传递的括号省略,因为它不是一个声明,当然也不是那种形式的声明。

关于聚合初始化:

正如 Johannes Schaub 在评论中指出的那样,只有以下语法[array.overview]/2可以保证您可以使用以下语法初始化std::array:

array<T, N> a = { initializer-list };

如果在形式T x { a };中允许花括号省略,您可以从中推断出以下语法:

array<T, N> a { initializer-list };

这段代码是格式良好的,并且具有相同的含义。然而,并不能保证std::array实际上包含一个原始数组作为其唯一的数据成员(也可以参见LWG 2310)。我认为一个例子可以是部分特化std::array<T, 2>,其中有两个数据成员T m0T m1。因此,不能得出结论

array<T, N> a {{ initializer-list }};

是格式良好的。不幸的是,这导致了这样一种情况,即没有保证的方法初始化一个std::array临时对象,而没有使用花括号省略T x { a };,这也意味着奇怪的例子(1、3、5、7)不需要工作。


所有这些初始化 std::array 的方式最终都会导致聚合初始化。它被定义为聚合成员的复制初始化。然而,使用花括号初始化列表的复制初始化仍然可以直接初始化聚合成员。例如:
struct foo { foo(int); foo(foo const&)=delete; };
std::array<foo, 2> arr0 = {1, 2};      // error: deleted copy-ctor
std::array<foo, 2> arr1 = {{1}, {2}};  // error/ill-formed, cannot initialize a
                                       // possible member array from {1}
                                       // (and too many initializers)
std::array<foo, 2> arr2 = {{{1}, {2}}}; // not guaranteed to work

第一个尝试从初始化程序列表中分别初始化数组元素12。这个复制初始化等同于foo arr0_0 = 1;,进而等同于foo arr0_0 = foo(1);,但这是不合法的(已删除复制构造函数)。
第二个不包含表达式列表,而是包含初始化程序列表,因此它不满足[array.overview]/2的要求。在实践中,std::array包含一个原始数组数据成员,该成员将仅从第一个初始化程序列表{1}初始化,然后第二个程序列表{2}是非法的。
第三个与第二个问题相反:如果有一个数组数据成员,则可以正常工作,但不能保证有这样的成员。

如果您指出示例1、3、5和7不保证工作(因为它们依赖于内部聚合结构/深度),我会点赞。 - Johannes Schaub - litb
@JohannesSchaub-litb 嗯..是的..那很糟糕。我重构了我的答案,并包含了我对您评论的理解(这导致了两个不幸的结论)。您是否知道是否有任何不使用原始数组成员的实现? - dyp

3

我认为它们都是严格符合要求的,除了可能的arr2。我会选择arr3的方式,因为它简洁、清晰,而且绝对有效。如果arr2是有效的(我不确定),那实际上会更好。

将括号和大括号组合在一起(0和1)从来没有让我感到舒服,等于号(4和5)还可以,但我更喜欢更短的版本,6和7则过于冗长。

然而,你可能想采用另一种方式,遵循Herb Sutter的"almost always auto"风格

auto arr8 = std::array<int, 3>{{1, 2, 3}};

我正在编写一个低级别且高度模板化的库,因此我尽可能避免使用关键字“auto”,因为我需要跟踪类型(但我同意在大多数情况下它可以是一种解决方案)。 - Vincent
在这种情况下使用 "auto" 看起来像编写 Python 代码,其中已知声明变量的类型是由分配给它的对象决定的。在这个例子中使用它也不错,但我更喜欢初始帖子中的第三个构造函数。我大多数时候都会用 "auto" 来声明循环中的迭代器。 - lucas92
@dyp的回答似乎表明std::array<int, 3>{{1, 2, 3}};不能保证可行,因为不确定std::array是否在内部使用C风格数组(因此{1,2,3}可能无法正确初始化其内部使用的任何内容)。 - M.M

1

这个链接指向一个错误报告,其中当使用-Wall时,默认不再启用-Wmissing-braces。如果你打开-Wmissing-bracesgcc会抱怨0、2、4和6(与clang相同)。

花括号省略可以在语句T a = { ... }中出现,但不能在T a { }中出现。

为什么C++的std::vector和std::array的initializer_list行为不同?

这是James McNellis的答案:

然而,这些额外的大括号只能在旧式等号使用的情况下被省略,即“T x = { a };”(C++11 §8.5.1/11)中声明时。允许省略括号的规则不适用于直接列表初始化。这里的一个脚注指出:“在其他列表初始化的用法中,无法省略大括号。” 有一个关于此限制的缺陷报告:CWG defect #1270。如果采纳建议的解决方案,则将允许省略括号以用于其他形式的列表初始化,... 如果采纳建议的解决方案,则将允许省略括号以用于其他形式的列表初始化,以下代码将是合法的: std::array y{ 1, 2, 3, 4 }; Xeo的回答:

std::array 没有接受 initializer_list 的构造函数。因此,它被视为聚合初始化。如果有这个构造函数,它看起来会像这样:

... 而 std::array 没有构造函数,而 {1, 2, 3, 4} 大括号初始化列表实际上不是解释为 std::initializer_list,而是用于 std::array 内部 C 风格数组的聚合初始化(这就是第二组大括号的来源:一组用于 std::array,另一组用于内部 C 风格成员数组)。

#include <array>
#include <initializer_list>

struct test {
    int inner[3];

    test(std::initializer_list<int> list) {
        std::copy(list.begin(), list.end(), inner);
    }
};

#include <iostream>

int main() { 
    test t{1, 2, 3};
    test t2({1, 2, 3});
    test t3 = {1, 2, 3};
    test t4 = test({1, 2, 3});
    for (int i = 0; i < 3; i++)
        std::cout << t.inner[i];
    for (int i = 0; i < 3; i++)
        std::cout << t2.inner[i];
    for (int i = 0; i < 3; i++)
        std::cout << t3.inner[i];
    for (int i = 0; i < 3; i++)
        std::cout << t4.inner[i];
}

0
最后两个是多余的:您可以在赋值的右侧使用数组声明的6种形式。此外,如果您的编译器没有优化掉复制操作,这些版本的效率会更低。
使用初始化列表构造函数需要双括号,因此您的第三行是无效的。

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