列表初始化(使用大括号)的优点是什么?

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

为什么?

258013 次浏览

基本上是从Bjarne Stroustrup的c++编程语言第四版;复制和粘贴:

列表初始化不允许窄化(§iso.8.5.4)。那就是:

  • 整数不能转换为不能保存其值的其他整数。例如,char 允许To int,但不允许int To 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

结论

优先选择{}初始化而不是其他选项,除非有充分的理由不这样做。

使用大括号初始化有很多原因,但你应该意识到initializer_list<>构造函数优先于其他构造函数,异常是默认构造函数。这导致了构造函数和模板的问题,其中T类型构造函数可以是初始化列表,也可以是普通的旧ctor。

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!!!
}

假设您没有遇到这样的类,那么几乎没有理由不使用初始化器列表。

关于使用列表初始化的优点,已经有了很好的答案,但我个人的经验法则是,尽可能不要使用花括号,而是根据概念意义来使用:

  • 如果我创建的对象在概念上保存了我在构造函数中传递的值(例如容器、POD结构体、原子、智能指针等),那么我使用的是大括号。
  • 如果构造函数类似于普通的函数调用(它执行一些由参数参数化的或多或少复杂的操作),那么我使用的是普通的函数调用语法。
  • 对于默认的初始化,我总是使用花括号。
    首先,这样我就能确保对象得到了初始化不管它是否是" real"类,使用默认构造函数(无论如何都会被调用)或内置/ POD类型。其次,在大多数情况下,它与第一个规则是一致的,因为默认初始化的对象通常表示“空”;李对象。< / >

根据我的经验,在默认情况下,这个规则集可以比使用大括号更一致地应用,但是当它们不能使用或与“normal”含义不同时,必须显式地记住所有异常;带圆括号的函数调用语法(调用不同的重载)。

例如,它很适合标准库类型,如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

只要你不像铬中的谷歌那样使用- wno -narrow构建,它就更安全。如果你这样做了,那就不太安全了。如果没有这个标志,c++ 20将会解决唯一不安全的情况。

< p >注意: A)大括号更安全,因为它们不允许缩窄。 B)大括号不太安全,因为它们可以绕过私有或删除的构造函数,隐式调用显式标记的构造函数

这两者结合起来意味着,如果里面是基本常量,它们会更安全,但如果是对象,它们就不那么安全了(尽管在c++ 20中进行了修正)。

< p >更新(2022-02-11): 请注意,在最初发布的(下面)的主题上,最近有更多的意见,反对{}初始化式的偏好,例如Arthur Dwyer在他的博客文章c++中初始化的噩梦.

最初的回答:

< p > # EYZ0阅读。 这将详细解释这些选项和其他几个选项之间的区别,以及与区分不同选项的行为相关的几个陷阱

摘自第四节的要点:

什么时候应该使用()vs.{}语法来初始化对象?为什么? 下面是简单的指导原则:

指导原则:首选使用{}初始化,例如vector V = {1,2,3,4};或者auto v = vector{1,2,3,4};,因为 它更一致,更正确,并且避免了必须知道 这完全是老式的陷阱。在你喜欢的单参数情况下 只看到=号,例如int I = 42;auto x =任何东西; 省略大括号是可以的。…< / p > 这涵盖了绝大多数情况。只有一个主干线 例外:< / p >

…在极少数情况下,如向量v(10,20);或者auto v = Vector(10,20);,使用初始化with()显式调用 构造函数,否则被initializer_list隐藏 构造函数。< / p > 然而,这通常应该是“罕见”的原因是因为默认 和复制构造已经是特殊的,并且可以很好地使用{}和 好的类设计现在大多避免了求助于-()的情况 用户定义的构造函数,因为这个最终的设计准则:

指导原则:当你设计一个类时,避免提供一个构造函数 使用initializer_list构造函数进行模糊重载,因此 用户将不需要使用()来访问这样一个隐藏的构造函数

也可以参考关于该主题的核心指南:ES.23:首选{}初始化式语法