使用花括号进行列表初始化有哪些优点?

617
MyClass a1 {a};     // clearer and less error-prone than the other three
MyClass a2 = {a};
MyClass a3 = a;
MyClass a4(a);

为什么?


59
没错,使用auto确实很方便,但我认为它会降低可读性 —— 当我阅读代码时,我喜欢“看到”对象的类型。如果你已经100%确定对象的类型,为什么还要使用auto呢?而且,如果你使用列表初始化(请看我的回答),你可以确保它始终正确。 - Oleksiy
159
@Oleksiy:std::map<std::string, std::vector<std::string>>::const_iterator 想要与你交流一下。 - Xeo
11
@Oleksiy 我建议阅读这篇 GotW - Rapptz
5
通常情况下,typedef std::map<std::string, std::vector<std::string>> MyContainer 是更加简洁的选择。对于本地范围类型,我只会使用auto - mip
25
我会说 using MyContainer = std::map<std::string, std::vector<std::string>>; 更好(特别是因为您可以将其模板化!) - JAB
显示剩余8条评论
5个回答

539

基本上是从Bjarne Stroustrup的《C++程序设计语言第四版》复制和粘贴的:

列表初始化不允许缩小转换 (§iso.8.5.4)。也就是说:

  • 一个整数不能被转换为另一个无法容纳其值的整数类型。例如,char到int是允许的,但是int到char是不允许的。
  • 浮点值不能转换为另一个浮点类型,该类型无法容纳其值。例如,float到double是允许的,但是double到float是不允许的。
  • 浮点值不能转换为整数类型。
  • 整数值不能转换为浮点类型。

例子:

void fun(double val, int val2) {

    int x2 = val;    // if val == 7.9, x2 becomes 7 (bad)

    char c2 = val2;  // if val2 == 1025, c2 becomes 1 (bad)

    int x3 {val};    // error: possible truncation (good)

    char c3 {val2};  // error: possible narrowing (good)

    char c4 {24};    // OK: 24 can be represented exactly as a char (good)

    char c5 {264};   // error (assuming 8-bit chars): 264 cannot be 
                     // represented as a char (good)

    int x4 {2.0};    // error: no double to int value conversion (good)

}

仅有在使用auto关键字获得由初始化程序确定的类型时,才优先选择=而不是{}。

例如:

auto z1 {99};   // z1 is an int
auto z2 = {99}; // z2 is std::initializer_list<int>
auto z3 = 99;   // z3 is an int

结论

除非你有充分的理由,否则请优先选择使用 {} 初始化方法。


62
使用括号()也可以被解析为函数声明,这是一个事实。使用T t(x,y,z);的语法形式却不能使用T t()的形式,这种不一致性很容易引起混淆。有时候,即使你已经明确了变量x的值,你也不能使用T t(x);的形式。 - juanchopanza
128
我强烈不同意这个答案;当您有一个接受std::initializer_list的构造函数类型时,大括号初始化会变得一团糟。RedXIII提到了这个问题(并只是随意地解决了它),而您完全忽略了它。A(5,4)A{5,4}可能调用完全不同的函数,这是需要知道的重要事情。这甚至可能导致看起来不直观的调用。说默认应该使用{}将导致人们误解发生的情况。虽然这不是您的错。我个人认为这是一个非常考虑不周的功能。 - user1520427
22
这就是为什么有“除非你有充分的理由不这样做”的部分。 - Oleksiy
87
虽然这个问题已经很老了,但它有相当多的点击量,因此我在这里添加一些内容供参考(我没有在页面上看到其他任何地方)。从C++14开始,使用新的花括号初始化列表自动推断规则,现在可以编写auto var{ 5 },它将被推断为int,而不再是std::initializer_list<int> - Edoardo Dominici
72
哈哈,从所有的评论中仍然不清楚该怎么做。确切的是,C++规范很混乱! - jaques-sam
显示剩余15条评论

191
关于使用列表初始化的优点已经有很好的答案了,但是我的个人经验法则是:尽可能不使用大括号,而是根据概念意义来决定使用哪种语法:
  • 如果我要创建的对象在概念上持有我传递给构造函数的值(例如容器、POD结构体、原子类型、智能指针等),那么我使用大括号。
  • 如果构造函数类似于普通函数调用(执行一些由参数参数化的更或多或少复杂的操作),则我使用普通函数调用语法。
  • 对于默认初始化,我总是使用大括号。首先,这样我始终可以确保对象被初始化,无论它是一个具有默认构造函数的“真实”类,还是一个内置/ POD类型。其次,在大多数情况下,这与第一条规则一致,因为默认初始化的对象通常表示一个“空”对象。
根据我的经验,这个规则集比默认使用大括号更加一致,但需要明确记住所有不能使用大括号或其含义与用括号的“正常”函数调用语法不同的例外情况。
它可以与标准库类型(例如std::vector)很好地配合使用。
vector<int> a{10, 20};   //Curly braces -> fills the vector with the arguments

vector<int> b(10, 20);   //Parentheses -> uses arguments to parametrize some functionality,                          
vector<int> c(it1, it2); //like filling the vector with 10 integers or copying a range.

vector<int> d{};      //empty braces -> default constructs vector, which is equivalent
                      //to a vector that is filled with zero elements

21
完全同意您的回答大部分观点。不过,您是否认为对于向量而言加上空括号只是多余的呢?我的意思是,当您需要对通用类型T的对象进行值初始化时,这样做是可以的,但在非通用代码中这样做有什么目的呢? - Mikhail
16
@Mikhail:这样做肯定有些多余,但是我习惯于始终显式地初始化本地变量。正如我所写的,这主要是为了保持一致性,以便在必要时不会忘记它的存在。当然,这并不是我在代码审查中会提到或放在样式指南中的内容。 - MikeMB
7
相当干净的规则集。 - laike9m
7
这是目前为止最好的答案。{}就像继承一样容易被滥用,导致代码难以理解。 - UKMonkey
6
@MikeMB 的例子:const int &b{} <- 并不是试图创建未初始化的引用,而是将其绑定到临时的整数对象上。第二个例子:struct A { const int &b; A():b{} {} }; <- 并不是试图创建未初始化的引用(因为()会这样做),而是将其绑定到临时的整数对象上,然后悬挂无用。即使使用-Wall选项,GCC也不会警告第二个例子。 - Johannes Schaub - litb
显示剩余8条评论

121
使用花括号初始化有很多原因,但需要注意的是,对于除了默认构造函数之外的其他构造函数而言,initializer_list<> 构造函数更受推荐。这会在类型为 T 的构造函数既可以是初始化列表也可以是旧式的构造函数时,引发构造函数和模板问题。
struct Foo {
    Foo() {}

    Foo(std::initializer_list<Foo>) {
        std::cout << "initializer list" << std::endl;
    }

    Foo(const Foo&) {
        std::cout << "copy ctor" << std::endl;
    }
};

int main() {
    Foo a;
    Foo b(a); // copy ctor
    Foo c{a}; // copy ctor (init. list element) + initializer list!!!
}

假设没有遇到这种类,那么就没有理由不使用初始化列表。


24
在泛型编程中,这是一个非常重要的观点。当你编写模板时,请不要使用花括号初始化列表(标准称为“{...}”),除非你想要使用initializer_list语义(以及可能用于默认构造对象)。 - Xeo
106
说实话,我不明白为什么要存在 std::initializer_list 规则,它只会使语言变得更加混乱和复杂。如果你想使用 std::initializer_list 构造函数,用 Foo{{a}} 不就可以了吗?这似乎比让 std::initializer_list 优先于所有其他重载函数更容易理解。 - user1520427
6
以上评论我点赞,因为我认为它非常混乱!它不合逻辑;对我而言,“Foo{{a}}”比“Foo{a}”更符合某种逻辑,后者会变成初始化列表优先级(用户可能会想什么...)。 - Gabriel
47
基本上,C++11用另一个混乱代替了一个混乱。哦,抱歉,它并没有取代原来的混乱——它只是增加了混乱。如果你没有遇到这些类,你怎么知道呢?如果你开始时没有std::initializer_list<Foo>构造函数,但是在某个时间点上要将其添加到Foo类中以扩展其接口,那么Foo类的用户就会出问题。 - mip
15
“使用大括号初始化的许多原因”是什么?这个答案指出了一个原因(initializer_list<>),但并没有说明谁说它是首选,然后又提到了一个不适用的好例子。我错过了什么,因为截至2016年4月21日约有30其他人发现它很有帮助? - dwanderson
显示剩余5条评论

10
更新(2022-02-11): 请注意,关于下面最初发布的偏好使用{}初始化程序的主题,有更近期的观点持反对意见,例如Arthur Dwyer在他的博客文章The Knightmare of Initialization in C ++中所述。
原回答:
请阅读Herb Sutter的(更新后的)GotW#1。 其中详细解释了这些区别以及其他几个选项,并介绍了几个相关的陷阱,以区分不同选项的行为。
从第4节提取/复制的要点:
何时使用 ( ) 和 { } 语法来初始化对象?为什么?
这是一个简单的指南:
指南:优先使用 { } 进行初始化,例如 vector v = {1, 2, 3, 4}; 或 auto v = vector{1, 2, 3, 4};,因为它更一致、更正确,而且完全避免了必须知道旧式陷阱的情况。在只有一个参数的情况下,如果您更喜欢只看到等号,例如 int i = 42; 和 auto x = anything;,省略大括号也可以。
这涵盖了绝大多数情况。只有一个主要例外:
在罕见的情况下,例如 vector v(10,20); 或 auto v = vector(10,20);,使用 ( ) 进行初始化以显式调用一个否则被 initializer_list 构造函数隐藏的构造函数。
然而,这通常应该是“罕见”的原因是默认和复制构造已经很特殊,并且可以与 { } 很好地配合使用,而且良好的类设计现在大多避免了用户定义的构造函数需要回到 ( ) 的情况,因为有以下最终设计指南:
指南:当您设计一个类时,避免提供与 initializer_list 构造函数重载模糊的构造函数,以便用户不需要使用 ( ) 来访问这种隐藏的构造函数。
此外,关于这个主题还请参考核心指南: ES.23: 更喜欢 {}-初始化语法

6
只要你不像Google在Chromium中那样使用-Wno-narrowing,它才更安全。如果你这样做了,那么它就不太安全。没有这个标志,唯一不安全的情况将在C++20中得到修复。
注意:
  1. 花括号更安全,因为它们不允许缩窄。
  2. 花括号不太安全,因为它们可以绕过私有或已删除的构造函数,并隐式调用显式标记的构造函数。
这两者结合起来意味着如果里面是原始常量,它们就更安全,但如果是对象,则不太安全(尽管在C++20中已修复)。

1
我尝试在goldbolt.org上使用提供的示例代码绕过“显式”或“私有”构造函数,将其中一个设置为私有或显式,并获得了相应的编译器错误。你能提供一些示例代码来支持这个吗? - Mark Storer
1
这是针对C++20提出的问题的修复方案:http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p1008r1.pdf - Allan Jensen
1
如果您编辑您的答案以显示您正在讨论哪个版本的C ++,我很乐意更改我的投票。 - Mark Storer
clang++ -std=c++14 告诉我:main.cpp:22:7: error: calling a private constructor of class 'Foo'。至于隐式调用显式构造函数的论断,其实根本毫无意义。这是一个隐式构造函数调用:foo_instance = false;。通过调用相应的构造函数,false 隐式地被转换为 Foo。如果您使用花括号,您将明确调用构造函数。重点是,如果您不提及类型名称,您不能使用花括号进行此类赋值。 - TamaMcGlinn

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