列表初始化 (C++11 起)

来自cppreference.com
< cpp‎ | language


 
 
C++ 语言
 
 

花括号包围的初始化器列表列表初始化对象。

语法

直接列表初始化

T 对象 { 实参1, 实参2, ... };

T 对象{.指派符1 = 实参1 , .指派符2 { 实参2 } ... };

(C++20 起)
(1)
T { 实参1, 实参2, ... }

T {.指派符1 = 实参1 , .指派符2 { 实参2 } ... }

(C++20 起)
(2)
new T { 实参1, 实参2, ... }

new T {.指派符1 = 实参1 , .指派符2 { 实参2 } ... }

(C++20 起)
(3)
{ T 成员 { 实参1, 实参2, ... }; };

{ T 成员 {.指派符1 = 实参1 , .指派符2 { 实参2 } ... }; };

(C++20 起)
(4)
::() : 成员 { 实参1, 实参2, ... } {...

::() : 成员 {.指派符1 = 实参1 , .指派符2 { 实参2 } ...} {...

(C++20 起)
(5)

复制列表初始化

T 对象 = { 实参1, 实参2, ... };

T 对象 = {.指派符1 = 实参1 , .指派符2 { 实参2 } ... };

(C++20 起)
(6)
函数 ({ 实参1, 实参2, ... })

函数 ({.指派符1 = 实参1 , .指派符2 { 实参2 } ... })

(C++20 起)
(7)
return { 实参1, 实参2, ... };

return {.指派符1 = 实参1 , .指派符2 { 实参2 } ... };

(C++20 起)
(8)
对象 [{ 实参1, 实参2, ... }]

对象 [{.指派符1 = 实参1 , .指派符2 { 实参2 } ... }]

(C++20 起)
(9)
对象 = { 实参1, 实参2, ... }

对象 = {.指派符1 = 实参1 , .指派符2 { 实参2 } ... }

(C++20 起)
(10)
U ({ 实参1, 实参2, ... })

U ({.指派符1 = 实参1 , .指派符2 { 实参2 } ... })

(C++20 起)
(11)
{ T 成员 = { 实参1, 实参2, ... }; };

{ T 成员 = {.指派符1 = 实参1 , .指派符2 { 实参2 } ... }; };

(C++20 起)
(12)

在下列情形进行列表初始化:

  • 直接列表初始化(考虑 explicit 和非 explicit 构造函数)
1) 以花括号包围的初始化器列表初始化具名变量
2) 以花括号包围的初始化器列表初始化无名临时量
3)new 表达式初始化具有动态存储期的对象,它的初始化器是花括号包围的初始化器列表
4) 在不使用等号的非静态数据成员初始化器
5) 在构造函数的成员初始化列表中,如果使用花括号包围的初始化器列表
  • 复制列表初始化(考虑 explicit 和非 explicit 构造函数,但只能调用非 explicit 构造函数)
6) 以等号后的花括号包围的初始化器列表初始化具名变量
7) 函数调用表达式中,以花括号包围的初始化器列表作为实参,以列表初始化对函数形参进行初始化
8) 在以花括号包围的初始化器列表作为返回表达式的 return 语句中,以列表初始化对返回的对象进行初始化
9) 在具有用户定义的 operator[]下标表达式中,以列表初始化对重载运算符的形参进行初始化
10)赋值表达式中,以列表初始化对重载的运算符的形参进行初始化
11) 函数式转型表达式或其他构造函数调用,其中花括号包围的初始化器列表用作构造函数实参。以复制初始化对构造函数的形参初始化(注意:此例中的类型 U 不是被列表初始化的类型;但 U 的构造函数的形参是)
12) 在使用等号的非静态数据成员初始化器

解释

(可有 cv 限定的)T 类型对象的列表初始化的效果是:

  • 如果花括号包围的初始化器列表包含指派初始化器列表,并且 T 不是引用类型,那么 T 必须是聚合类。由指派初始化器列表中的指派符组成的标识符序列必须是 T 的非静态数据成员组成的标识符序列的子序列。进行聚合初始化
(C++20 起)
  • 如果 T 是聚合类且花括号包围的初始化器列表在不含指派初始化器列表的情况下 (C++20 起)拥有单个(可有 cv 限定的)相同类型或派生类型的初始化器子句,那么从该子句初始化对象(对于复制列表初始化为复制初始化,对于直接列表初始化为直接初始化)。
  • 否则,如果 T 是字符数组且花括号包围的初始化器列表拥有单个类型适当的字符串字面量初始化器子句,那么照常从字符串字面量初始化数组。
  • 否则,如果花括号包围的初始化器列表为空,且 T 是拥有默认构造函数的类类型,那么进行值初始化
  • 否则,如果 T 是类类型,则以两个阶段考虑 T 的构造函数:
  • 如果上一个阶段未产生匹配,那么 T 的所有构造函数都参与针对由花括号包围的初始化器列表的各初始化器子句所组成的实参集的重载决议,它会受到只允许非窄化转换的限制。如果这个阶段为复制列表初始化产生的最佳匹配是 explicit 构造函数,那么编译失败(注意:简单复制初始化中完全不考虑 explicit 构造函数)。
  • 否则,如果 T 是拥有固定底层类型 U枚举类型,花括号包围的初始化器列表只有一个初始化器 v,并且满足以下所有条件,那么就会以将 v 转换到 U 的结果初始化该枚举:
    • 初始化是直接列表初始化。
    • v 具有标量类型
    • v 可隐式转换到 U
    • vU 的转换不是窄化转换。
(C++17 起)
  • 否则(如果 T 不是类类型),如果花括号包围的初始化器列表只有一个初始化器子句,且 T 要么不是引用类型,要么是引用类型而它所引用的类型与该子句的类型相同或者是它的基类,那么直接初始化(直接列表初始化时)或复制初始化(复制列表初始化时)这个 T,但不允许窄化转换。
  • 否则,如果 T 是与初始化器子句类型不兼容的引用类型,那么:
  • 复制列表初始化一个具有由 T 引用的类型的纯右值临时量,并且将引用绑定到该临时量(如果引用是非 const 左值引用则失败)。
(C++17 前)
  • 生成一个纯右值。该纯右值以复制列表初始化来初始化它的结果对象。然后用该纯右值直接初始化引用(如果引用是非 const 左值引用则会失败)。该临时量的类型是由 T 引用的类型,除非 T 是“到 U 的未知边界的数组的引用”,此时该临时量的类型是声明 U x[] Hx 的类型,其中 H 是对应的初始化器列表 (C++20 起)
(C++17 起)
  • 否则,如果花括号包围的初始化器列表没有任何初始化器子句,那么值初始化 T

列表初始化 std::initializer_list

类型是 std::initializer_list<E> 的对象从初始化器列表构造时,编译器如同生成实质化 (C++17 起)一个类型是“包含 Nconst E 元素的数组”的纯右值,其中 N 是初始化器列表中的初始化器子句个数;该数组被称为该初始化器列表的基底数组

基底数组中的每个元素都会以初始化器列表中的对应初始化器子句复制初始化,并且构造的 std::initializer_list<E> 的对象会指代基底数组。复制操作选择的构造函数或转换函数必须在初始化器列表的语境下可访问。如果初始化元素时需要进行窄化转换,那么程序非良构。

基底数组的生存期与其他临时对象基本一样,但区别在从基底数组初始化 std::initializer_list 对象时会与绑定引用到临时量一样延续基底数组的生存期。

void f(std::initializer_list<double> il);
 
void g(float x)
{
   f({1, x, 3});
}
 
void h()
{
   f({1, 2, 3});
}
 
struct A { mutable int i; };
 
void q(std::initializer_list<A>);
 
void r()
{
    q({A{1}, A{2}, A{3}});
}
 
// 以上初始化会以大致与以下相同的方式实现,前提是编译器可以以一对指针构造
// initializer_list 对象,并且 __b 的生存期不会持续到 f 的调用以外。
 
void g(float x)
{
    const double __a[3] = {double{1}, double{x}, double{3}}; // 基底数组
    f(std::initializer_list<double>(__a, __a + 3));
}
 
void h()
{
    static constexpr double __b[3] =
        {double{1}, double{2}, double{3}}; // 基底数组
    f(std::initializer_list<double>(__b, __b + 3));
}
 
void r()
{
    const A __c[3] = {A{1}, A{2}, A{3}}; // 基底数组
    q(std::initializer_list<A>(__c, __c + 3));
}

未指定基底数组是否各异(即它们各自在不重叠的对象中存储):

bool fun(std::initializer_list<int> il1, std::initializer_list<int> il2)
{
    return il2.begin() == il1.begin() + 1;
}
 
bool overlapping = fun({1, 2, 3}, {2, 3, 4}); // 结果未指定:两个基底数组可以
                                              // 在 {1, 2, 3, 4} 上共享存储

窄化转换

列表初始化通过禁止下列转换对隐式转换加以限制:

  • 从浮点类型到整数类型的转换
  • 从一个浮点类型 T 到另一个浮点转换等级既不高于也不等于 T 的等级的浮点类型的转换,除非转换结果是常量表达式并且满足以下任一条件:
    • 转换后的值是有限值,并且转换没有溢出。
    • 转换前后的值都是无限值。
  • 从整数类型到浮点类型的转换,除非来源是能将它的值完全存储于目标类型的常量表达式
  • 从整数或无作用域枚举类型到不能表示原类型所有值的整数类型的转换,除非
    • 来源是宽度(记为 w)小于自身类型(或者对于枚举类型来说是底层类型)的位域并且目标类型可以表示宽度是 w 且符号与原类型一致的虚设扩展整数类型的所有值,或者
    • 来源是能将它的值完全存储于目标类型的常量表达式
  • 从指针或成员指针类型到 bool 的转换

注解

每个初始化器子句均按顺序早于花括号包围的初始化器列表中后随于它的任何初始化器子句。这与无顺序的 (C++17 前)顺序不确定 (C++17 起)函数调用表达式不同。

花括号包围的初始化器列表不是表达式,因此它没有类型,即 decltype({1, 2}) 非良构。没有类型意味着模板类型推导无法推导出与花括号包围的初始化器列表相匹配的类型,因此给定声明 template<class T> void f(T); 则表达式 f({1, 2, 3}) 非良构。然而,模板形参可以另行推导,如 std::vector<int> v(std::istream_iterator<int>(std::cin), {}) 的情况,其中迭代器类型从首个实参推导,但也被用于第二形参位置。对于使用关键词 auto 的类型推导中有一个例外,在复制列表初始化中将任何花括号包围的初始化器列表均推导为 std::initializer_list

同样因为花括号包围的初始化器列表没有类型,所以在将它用作重载函数调用的实参时,适用重载决议的特殊规则

聚合体直接从同类型的单初始化器子句花括号包围的初始化器列表进行复制/移动初始化,但非聚合体首先考虑 std::initializer_list 构造函数:

struct X {}; // 聚合体
 
struct Q     // 非聚合体
{
    Q() = default;
    Q(Q const&) = default;
    Q(std::initializer_list<Q>) {}
};
 
int main()
{
    X x;
    X x2 = X{x}; // 复制构造函数(非聚合初始化)
 
    Q q;
    Q q2 = Q{q}; // initializer_list 构造函数(非复制构造函数)
}

有些编译器(例如 gcc 10)仅在 C++20 模式认为从指针或成员指针到 bool 的转换是窄化的。

功能特性测试宏 标准 功能特性
__cpp_initializer_lists 200806L (C++11) 列表初始化和 std::initializer_list

示例

#include <iostream>
#include <map>
#include <string>
#include <vector>
 
struct Foo
{
    std::vector<int> mem = {1, 2, 3}; // 非静态成员的列表初始化
    std::vector<int> mem2;
    Foo() : mem2{-1, -2, -3} {} // 构造函数中的成员列表初始化
};
 
std::pair<std::string, std::string> f(std::pair<std::string, std::string> p)
{
    return {p.second, p.first}; // return 语句中的列表初始化
}
 
int main()
{
    int n0{};  // 值初始化(为零)
    int n1{1}; // 直接列表初始化
 
    std::string s1{'a', 'b', 'c', 'd'}; // initializer_list 构造函数调用
    std::string s2{s1, 2, 2};           // 常规构造函数调用
    std::string s3{0x61, 'a'}; // initializer_list 构造函数偏好 (int, char)
 
    int n2 = {1}; // 复制列表初始化
    double d = double{1.2}; // 纯右值的列表初始化,然后复制初始化
    auto s4 = std::string{"HelloWorld"}; // 同上, C++17 起不创建临时对象
 
    std::map<int, std::string> m = // 嵌套列表初始化
    {
        {1, "a"},
        {2, {'a', 'b', 'c'}},
        {3, s1}
    };
 
    std::cout << f({"hello", "world"}).first // 函数调用中的列表初始化
              << '\n';
 
    const int (&ar)[2] = {1, 2}; // 绑定左值引用到临时数组
    int&& r1 = {1}; // 绑定右值引用到临时 int
//  int& r2 = {2}; // 错误:不能绑定右值到非 const 左值引用
 
//  int bad{1.0}; // 错误:窄化转换
    unsigned char uc1{10}; // 可以
//  unsigned char uc2{-1}; // 错误:窄化转换
 
    Foo f;
 
    std::cout << n0 << ' ' << n1 << ' ' << n2 << '\n'
              << s1 << ' ' << s2 << ' ' << s3 << '\n';
    for (auto p : m)
        std::cout << p.first << ' ' << p.second << '\n';
    for (auto n : f.mem)
        std::cout << n << ' ';
    for (auto n : f.mem2)
        std::cout << n << ' ';
 
    [](...){}(d, ar, r1, uc1); // 效果同 [[maybe_unused]]
}

输出:

world
0 1 1
abcd cd aa
1 a
2 abc
3 abcd
1 2 3 -1 -2 -3

缺陷报告

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

缺陷报告 应用于 出版时的行为 正确行为
CWG 1288 C++11 以单初始化器子句的花括号包围的初始化器
列表初始化引用时只能绑定引用到临时量
合法时会直接绑定到列表
里的那个初始化器子句
CWG 1290 C++11 未正确指定基底数组的生存期 指定为与临时对象相同
CWG 1301 C++11 对源自 {} 的初始化首先考虑值初始化 首先考虑聚合初始化
CWG 1418 C++11 基底数组的类型缺少 const 添加 const
CWG 1467 C++11 禁止同类型的聚合体和字符数组初始化;
单子句列表的初始化器列表构造函数优先级高于复制构造函数
允许同类型初始化;
单子句列表直接初始化
CWG 1494 C++11 当以类型不兼容的初始化器子句列表初始化引用时,不确定会
直接列表初始化还是复制列表初始化新建的临时量
取决于该引用的初始化类别
CWG 2137 C++11 初始化器列表构造函数在从 {X} 初始化时失去复制构造函数的优先级 非聚合体首先考虑初始化器列表
CWG 2252 C++17 枚举可以从非标量值列表初始化 已禁止
CWG 2267 C++11 CWG 问题 1494 的解决方案明确了临时量可以被直接列表初始化 在列表初始化引用时它们
会被复制列表初始化
CWG 2374 C++17 枚举的直接列表初始化允许了过多的源类型 已限制
CWG 2627 C++11 具有较大整数类型的窄位域可以提升到一个较小整数类型,但是依然是窄化转换 不是窄化转换
CWG 2713 C++98 不能以指派初始化式列表初始化到聚合类的引用 可以初始化
CWG 2830 C++11 列表初始化没有忽略顶层 cv 限定性 会忽略
CWG 2864 C++11 有溢出的浮点转换不是窄化转换 是窄化转换
P1957R2 C++11 从指针/成员指针到 bool 的转换不是窄化转换 认为它是窄化转换
P2752R3 C++11 生存期重叠的多个基底数组不能重叠 它们可以重叠

参阅