为什么列表初始化(使用花括号)比替代品更好?

Ole*_*siy 349 c++ syntax initialization c++11 list-initialization

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

为什么?

我在SO上找不到答案,所以让我回答一下我自己的问题.

Ole*_*siy 311

基本上是从Bjarne Stroustrup的"The C++ Programming Language 4th Edition"中复制和粘贴:

列表初始化不允许缩小(§iso.8.5.4).那是:

  • 整数不能转换为另一个不能保持其值的整数.例如,允许char到int,但不允许int到char.
  • 浮点值无法转换为无法保持其值的另一个浮点类型.例如,允许浮动为double,但不允许浮动为double.
  • 浮点值无法转换为整数类型.
  • 整数值无法转换为浮点类型.

例:

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)

}
Run Code Online (Sandbox Code Playgroud)

=优先于{} 的唯一情况是使用auto关键字来获取初始化程序确定的类型.

例:

auto z1 {99};   // z1 is an int
auto z2 = {99}; // z2 is std::initializer_list<int>
auto z3 = 99;   // z3 is an int
Run Code Online (Sandbox Code Playgroud)

结论

除非您有充分的理由不这样做,否则首选初始化替代方案.

  • 我非常不同意这个答案; 当你有一个ctor接受`std :: initializer_list`的类型时,braced初始化变得完全混乱.RedXIII提到了这个问题(并且只是将其刷掉),而你完全忽略了它.`A(5,4)`和`A {5,4}`可以调用完全不同的函数,这是一个重要的事情要知道.它甚至可能导致看起来不直观的呼叫.说默认情况下你应该更喜欢`{}`会导致人们误解正在发生的事情.不过,这不是你的错.我个人认为这是一个非常糟糕的想法. (68认同)
  • 虽然这个问题已经很久了,但它有很多内容,因此我在这里添加它只是为了参考(我还没有在页面的其他地方看到它).从C++ 14开始,新的[从braced-init-list中自动扣除规则](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n3922.html)现在可以编写`auto var {5}`,它将被推断为`int`不再是`std :: initializer_list <int>`. (59认同)
  • 还有一个事实是使用`()`可以解析为函数声明.令人困惑和不一致的是你可以说`T t(x,y,z);`但不是'T t()`.有时,你确定`x`,你甚至不能说`T t(x);`. (46认同)
  • @ user1520427这就是为什么有"*除非你有充分的理由不要*"的一部分. (10认同)
  • @ user1520427我看到你在说什么,但在这种情况下,接受`std :: initializer_list`的构造函数是**唯一**考虑替代品的原因.在**每个**其他场景中,首选`{}`初始化.如果我不明白,请告诉我. (10认同)
  • 哈哈,从所有评论来看,目前尚不清楚该怎么做。清楚的是,C ++规范是一团糟! (7认同)
  • “总是从桥上跳下来,除非你有充分的理由不这样做”@user 我是这样解释的。 (5认同)
  • @Oleksiy我认为总是有充分的理由考虑替代方案,特别是当简单地添加`std :: initializer_list`构造函数可以改变现有`{}`调用的语义时.老实说,我不明白你怎么能得出你所做的结论,但也许这只是因为我认为这部分语言需要付出更多的努力.那好吧. (4认同)
  • 现在使用auto的示例不正确.它应该改为:z1是int,z2是int,`auto z3 = {99}; // z3是一个std :: initializer_list <int>`. (4认同)
  • 我不认为这解释了为什么它更好。问题不仅是因为您假设它是加载的,而且答案只是说明了 {} 类型的初始化转换是如何工作的;这并不比调用函数时所做的转换更安全。一个更糟的完美例子是`struct X{int i; 国际 j}; X {5};` 将编译;但让 j 未初始化 (3认同)
  • @EdoardoSparkonDominici 这一变化并未进入 C++14;直到 C++17 才被添加。 (2认同)
  • @juanchopanza 列表初始化有其自身的缺点。你可以说“T &amp;t{otherT}”,但不能说“T &amp;t{otherT, somethingElse}”,但你*可以*说“const T&amp; t{otherT}”和“const T&amp; t{otherT, somethingElse}”。然而在第一种情况下,`&amp;t == &amp;otherT`,但在第二种情况下,有一个临时创建。旧式初始化始终防止这种危险,只会使多参数变体格式错误。 (2认同)

Mik*_*eMB 91

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

  • 如果我正在创建的对象在概念上保存了我在构造函数中传递的值(例如容器,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
Run Code Online (Sandbox Code Playgroud)

  • 完全同意你的大部分答案.但是,你不觉得把向量的空括号放在一边是多余的吗?我的意思是,当你需要对泛型类型T的对象进行值初始化时,它是可以的,但是为非泛型代码执行它的目的是什么? (9认同)
  • @MikeMB 示例:`const int &amp;b{}` &lt;- 不会尝试创建未初始化的引用,而是将其绑定到临时整数对象。第二个例子:`struct A { const int &amp;b; A():b{} {} };` &lt;- 不会尝试创建一个未初始化的引用(就像 `()` 会做的那样),而是将它绑定到一个临时的整数对象,然后让它悬空。GCC 即使使用 `-Wall` 也不会对第二个示例发出警告。 (5认同)
  • @Mikhail:这当然是多余的,但我的习惯是始终使局部变量初始化显式化.正如我所写的那样,这主要是关于相关性,所以当它确实重要时我不会忘记它.这当然不是我在代码审查中提到的,也不是在风格指南中提到的. (4认同)
  • @JohannesSchaub-litb。该死的,完全忘记了,但我仍然很难想象这是一个问题。您的第二个示例的构造函数显然已损坏(对于显然的某些定义),因此它是类设计中的错误 - 不在指南中(clang 甚至给出错误)。如果您省略构造函数(使其成为 POD),您将在实例化时收到错误消息。第一个例子应该不是问题,因为引用应该延长临时的生命周期 - 不是吗? (4认同)
  • 我个人甚至更进一步:`std::vector&lt;int&gt; v({10, 12});`... (4认同)
  • 非常干净的规则集. (3认同)
  • 这是迄今为止最好的答案.{}就像继承 - 易于滥用,导致难以理解的代码. (2认同)
  • 您好,就像您的回答一样,但是如果使用某些第三方库,您可能无法确定该库内部对构造函数参数的真正作用,您会怎么做?无论如何,使用这种方法时,我们需要了解 lib 的内部结构,而不能仅仅将其视为 API 级别的黑盒。我发现一般来说,C++ 在这里并没有尽力而为 - 语法需要更多改进。 (2认同)

Red*_*III 86

有两种使用大括号初始化的原因有很多,但你应该知道,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!!!
}
Run Code Online (Sandbox Code Playgroud)

假设您没有遇到此类,则没有理由不使用初始化列表.

  • 老实说,我不明白为什么`std :: initializer_list`规则存在 - 它只会增加语言的混乱和混乱.如果你想要`std :: initializer_list`构造函数,那么做`Foo {{a}}`有什么问题?这比使用`std :: initializer_list`优先于所有其他重载更容易理解. (78认同)
  • 基本上C++ 11用另一个混乱代替了一个混乱.哦,对不起,它不会取代它 - 它增加了它.你怎么知道你是否遇到过这样的课程?如果你没有**`std :: initializer_list <Foo>`构造函数启动**会怎么样,但是在某些时候它会被添加到`Foo`类以扩展它的界面?然后,'Foo`类的用户被搞砸了. (31认同)
  • 这是通用编程中非常重要的一点.当你编写模板时,*不要*使用braced-init-lists(`{...}`的标准名称)除非你想要`initializer_list`语义(好吧,也许是默认构造一个对象). (18认同)
  • ..什么是"使用大括号初始化的许多原因"?这个答案指出了一个原因(`initializer_list <>`),它没有真正的资格****谁说它是首选的,然后继续提到一个好的情况,它不是**首选.我错过了什么〜其他30人(截至2016-04-21)发现有帮助? (9认同)
  • 以上评论为+​​1,因为我觉得它真的很乱!这不是逻辑; ``Foo {{a}}``为我提供了一些逻辑远远超过``Foo {a}``变成初始化列表优先级(用户可能认为hm ......) (5认同)
  • `Foo c {a}`从CWG 1467开始调用复制文件(至少在clang中实现) (5认同)
  • 那些"很多原因"是什么? (2认同)
  • @Doc:您可以对任何函数这样说:“如果稍后添加更好的匹配重载怎么办?” (2认同)

cod*_*ing 11

更新 (2022-02-11):\n请注意,与最初发布的观点相比,最近有更多关于该主题的观点(如下),这些观点反对 {} 初始化程序的偏好,例如 Arthur Dwyer 在他关于 The 的博客文章中C++ 初始化的噩梦

\n

原答案:

\n

阅读Herb Sutter 的(更新)GotW #1。\n这详细解释了这些选项和其他一些选项之间的区别,以及与区分不同选项的行为相关的几个陷阱。

\n

要点/复制自第 4 节:

\n
\n

什么时候应该使用 ( ) 与 { } 语法来初始化对象?为什么?\n这里\xe2\x80\x99s 是简单的指导原则:

\n

指导原则:优先使用 { } 进行初始化,例如 vector\nv = { 1, 2, 3, 4 }; 或 auto v = vector{ 1, 2, 3, 4 };,因为 \nit\xe2\x80\x99 更一致、更正确,并且完全避免了了解旧式陷阱。在单参数情况下,\n您希望只看到 = 符号,例如 int i = 42; 并且 auto x = everything;\ 省略大括号就可以了。\xe2\x80\xa6

\n

这涵盖了绝大多数情况。只有一个主要\n例外:

\n

\xe2\x80\xa6 极少数情况下,例如向量 v(10,20);或 auto v =\nvector(10,20);,使用带 ( ) 的初始化来显式调用\n构造函数,否则该构造函数将被initializer_list\n构造函数隐藏。

\n

然而,这通常应该是 \xe2\x80\x9crare\xe2\x80\x9d 的原因是因为默认\n和复制构造已经很特殊并且可以与 { } 一起正常工作,并且\n良好的类设计现在大多避免诉诸-由于最终设计准则,用户定义的构造函数\n为 ( ) 情况:

\n

指南:当您设计一个类时,避免提供一个使用initializer_list 构造函数进行重载的构造函数,这样用户就不需要使用 ( ) 来访问这样的隐藏构造函数。

\n
\n

另请参阅有关该主题的核心指南:ES.23:首选 {}-初始化语法

\n


小智 6

只要你不使用-Wno-narrowing像谷歌在 Chromium 中所做的那样构建,它就会更安全。如果你这样做,那就不太安全了。如果没有该标志,唯一的不安全情况将由 C++20 修复。

笔记:

  1. 大括号更安全,因为它们不允许缩小。
  2. 大括号不太安全,因为它们可以绕过私有或删除的构造函数,并隐式调用显式标记的构造函数。

这两个组合意味着如果里面是原始常量,它们会更安全,但如果它们是对象,则不太安全(尽管在 C++20 中已修复)