聚合初始化

来自cppreference.com
< cpp‎ | language

从花括号初始化器列表初始化聚合体。

语法

T 对象 = {实参1, 实参2, ...}; (1)
T 对象 {实参1, 实参2, ... }; (2) (C++11 起)
T 对象 = { .指派符 = 实参1 , .指派符 { 实参2 } ... }; (3) (C++20 起)
T 对象 { .指派符 = 实参1 , .指派符 { 实参2 } ... }; (4) (C++20 起)
T 对象 (实参1, 实参2, ...); (5) (C++20 起)

解释

聚合初始化对聚合体进行初始化。它是列表初始化 (C++11 起),或直接初始化 (C++20 起)的一种形式。

聚合体是下列类型之一:

  • 数组类型
  • 符合以下条件的类类型(常为 structunion
  • 没有私有或受保护的直接 (C++17 起)非静态数据成员
  • 没有用户声明的构造函数
(C++11 前)
  • 没有用户提供的构造函数(允许显式预置或弃置的构造函数)
(C++11 起)
(C++17 前)
  • 没有用户提供、继承或 explicit 的构造函数(允许显式预置或弃置的构造函数)
(C++17 起)
(C++20 前)
  • 没有用户声明或继承的构造函数
(C++20 起)
  • 没有虚、私有或受保护 (C++17 起)的基类
  • 没有虚成员函数
(C++11 起)
(C++14 前)

聚合初始化的效果是:

  • 每个直接公开基类、 (C++17 起)数组元素或非静态类成员,以数组下标/出现于类定义的顺序,从初始化器列表中的对应子句进行复制初始化
  • 如果初始化器子句是表达式,那么根据复制初始化允许进行隐式转换,但对于列表初始化形式禁止窄化转换 (C++11 起)
  • 如果初始化器子句是嵌套的花括号初始化器列表(它不是表达式),那么它对应的数组元素/类成员/公开基类 (C++17 起)从该子句进行列表初始化:聚合初始化是递归的。
  • 如果对象是未知大小的数组,且所提供的花括号环绕的初始化器列表拥有 n 个子句,那么该数组的大小是 n。(注意此时对象不能是非静态数据成员:成员必须拥有完整类型。)
  • 静态数据成员和无名位域在聚合初始化中被跳过。
  • 如果初始化器子句的数量超出所要初始化的成员和基类 (C++17 起)的数量,那么程序非良构。
  • 如果初始化器子句的数量少于成员的数量,或初始化器列表完全为空,那么剩余成员被值初始化。如果这些剩余成员的类型中有引用类型,那么程序非良构。
(C++11 前)
  • 如果初始化器子句的数量少于成员和基类 (C++17 起)的数量,或初始化器列表完全为空,那么剩余成员和基类 (C++17 起)会按照通常的列表初始化规则(对非类类型和拥有默认构造函数的非聚合类进行值初始化,并对聚合体进行聚合初始化),在类定义中有提供默认成员初始化器的情况下用它初始化,否则 (C++14 起)从空列表复制初始化。如果这些剩余成员的类型中有引用类型,那么程序非良构。
(C++11 起)
  • 当聚合初始化联合体时,只初始化其首个非静态数据成员。

可消除(省略)环绕嵌套的初始化器列表的花括号,这种情况下,使用所需数量的初始化器子句初始化对应的子聚合体的各个成员或元素,而后继的各个初始化器子句被用于初始化对象中的后续成员。然而,如果对象拥有不带任何成员的子聚合体(空结构体,或只保有静态成员的结构体),则不允许消除花括号而必须使用一个空的嵌套列表 {}

指派初始化器

语法形式 (3,4) 被称为指派初始化器:每个 指派符 必须指名 T 的一个直接非静态数据成员,而表达式中所用的所有 指派符 必须按照与 T 的数据成员相同的顺序出现。

struct A { int x; int y; int z; };
A a{.y = 2, .x = 1}; // 错误:指派符的顺序不匹配声明顺序
A b{.x = 1, .z = 2}; // OK:b.y 被初始化为 0

指派初始化器所指名的每个直接非静态数据成员,从其指派符后随的对应花括号或等号初始化器初始化。禁止窄化转换。

指派初始化器可用于将联合体初始化为其首个成员之外的状态。只可以为一个联合体提供一个初始化器。

union u { int a; const char* b; };
u f = { .b = "asdf" };         // OK:联合体的活跃成员为 b
u g = { .a = 1, .b = "asdf" }; // 错误:只可提供一个初始化器

对于非联合体的聚合体中未提供指派初始化器的元素,按上述针对初始化器子句的数量少于成员数量时的规则进行初始化(如果提供默认成员初始化器则使用它,否则为空列表初始化):

struct A {
  string a;
  int b = 42;
  int c = -1;
};
A{.c=21}  // 以 {} 初始化 a,这样会调用默认构造函数
          // 然后以 = 42 初始化 b
          // 然后以 = 21 初始化 c

如果以指派初始化器子句初始化的聚合体拥有一个匿名联合体成员,那么对应的指派初始化器必须指名该匿名联合体的其中一个成员。

注意:乱序的指派初始化、嵌套的指派初始化、指派初始化器与常规初始化器的混合,以及数组的指派初始化在 C 编程语言中受支持,但在 C++ 不允许。

struct A { int x, y; };
struct B { struct A a; };
struct A a = {.y = 1, .x = 2}; // 合法 C,非法 C++(乱序)
int arr[3] = {[1] = 5};        // 合法 C,非法 C++(数组)
struct B b = {.a.x = 0};       // 合法 C,非法 C++(嵌套)
struct A a = {.x = 1, 2};      // 合法 C,非法 C++(混合)
(C++20 起)

字符数组

通常字符类型(charsigned charunsigned charchar8_t (C++20 起)char16_tchar32_t (C++11 起)wchar_t 的数组能分别从通常字符串字面量、 UTF-8 字符串字面量 (C++20 起)、 UTF-16 字符串字面量、 UTF-32 字符串字面量 (C++11 起)或宽字符串字面量初始化,可以以花括号环绕。字符串字面量的相继字符(包含隐含的空终止字符)初始化各数组元素。如果数组大小被指定且大于字符串字面量中的字符数,那么剩余字符会被零初始化。

char a[] = "abc";
// 等价于 char a[4] = {'a', 'b', 'c', '\0'};
 
//  unsigned char b[3] = "abc"; // 错误:初始化器字符串太长
unsigned char b[5]{"abc"};
// 等价于 unsigned char b[5] = {'a', 'b', 'c', '\0', '\0'};
 
wchar_t c[] = {L"кошка"}; // 可选的花括号
// 等价于 wchar_t c[6] = {L'к', L'о', L'ш', L'к', L'а', L'\0'};

注解

聚合类或数组可以包含非聚合的公开基类、 (C++17 起)成员或元素,它们以上述方式初始化(例如从对应的初始化器子句复制初始化)

C++11 前,聚合初始化中曾允许窄化转换,但此后不再被允许,但从 C++20 起聚合初始化使用圆括号时允许它们。

C++11 前,由于语法限制,聚合初始化只能用于变量定义,而不能用于构造函数初始化器列表new 表达式或临时对象创建。

C 中,长度比字符串字面量的大小少一的字符数组可以从字符串字面量初始化;产生的数组是非空终止的。这在 C++ 中不允许。

示例

#include <string>
#include <array>
struct S {
    int x;
    struct Foo {
        int i;
        int j;
        int a[3];
    } b;
};
 
union U {
    int a;
    const char* b;
};
 
int main()
{
    S s1 = { 1, { 2, 3, {4, 5, 6} } };
    S s2 = { 1, 2, 3, 4, 5, 6}; // 相同,但有花括号消除
    S s3{1, {2, 3, {4, 5, 6} } }; // 相同,使用直接列表初始化语法
    S s4{1, 2, 3, 4, 5, 6}; // CWG 1270 前错误:花括号消除仅允许与等号一起使用
 
    int ar[] = {1,2,3}; // ar 为 int[3]
    int ab[] (1, 2, 3); // (C++20) ab 为 int[3] 
//  char cr[3] = {'a', 'b', 'c', 'd'}; // 过多初始化器子句
    char cr[3] = {'a'}; // 数组初始化为 {'a', '\0', '\0'}
 
    int ar2d1[2][2] = {{1, 2}, {3, 4}}; // 完全花括号的 2D 数组: {1, 2}
                                        //                      {3, 4}
    int ar2d2[2][2] = {1, 2, 3, 4}; // 花括号消除: {1, 2}
                                    //             {3, 4}
    int ar2d3[2][2] = {{1}, {2}};   // 仅第一列: {1, 0}
                                    //           {2, 0}
 
    std::array<int, 3> std_ar2{ {1,2,3} };    // std::array 是聚合体
    std::array<int, 3> std_ar1 = {1, 2, 3}; // 可以消除花括号
 
    int ai[] = { 1, 2.0 }; // 从 double 到 int 的窄化转换:
                           // C++11 中错误,C++03 中 OK
 
    std::string ars[] = {std::string("one"), // 复制初始化
                         "two",              // 转换,然后复制初始化
                         {'t', 'h', 'r', 'e', 'e'} }; // 列表初始化
 
    U u1 = {1}; // OK,联合体的首个成员
//    U u2 = { 0, "asdf" }; // 错误:用过多的初始化器初始化联合体
//    U u3 = { "asdf" }; // 错误:到 int 的转换无效
 
    [](auto...) { std::puts("废弃无用的变量。。。 完成。"); } (
        s1, s2, s3, s4, ar, ab, cr, ar2d1, ar2d2, ar2d3, std_ar2, std_ar1, u1
    );
}
 
// 聚合体
struct base1 { int b1, b2 = 42; };
// 非聚合体
struct base2 {
  base2() : b3(42) {}
  int b3;
};
// C++17 里是聚合体
struct derived : base1, base2 { int d; };
derived d1{ {1, 2}, { }, 4}; // d1.b1 = 1, d1.b2 = 2,  d1.b3 = 42, d1.d = 4
derived d2{ {    }, { }, 4}; // d2.b1 = 0, d2.b2 = 42, d2.b3 = 42, d2.d = 4

输出:

废弃无用的变量。。。 完成。

缺陷报告

下列更改行为的缺陷报告追溯地应用于以前出版的 C++ 标准。

DR 应用于 出版时的行为 正确行为
CWG 1270 C++11 仅允许在复制列表初始化中使用花括号消除 在别处也允许

参阅