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

What are the advantages of list initialization (using curly braces)?

提问人:Oleksiy 提问时间:8/14/2013 最后编辑:HolyBlackCatOleksiy 更新时间:7/21/2023 访问量:297303

问:

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

为什么?

C++ C++11 语法 初始化 列表初始化

评论

59赞 Oleksiy 8/14/2013
没错,这很方便,但在我看来,它降低了可读性——我喜欢在阅读代码时查看对象是什么类型。如果您 100% 确定对象是什么类型,为什么要使用 auto?如果你使用列表初始化(阅读我的答案),你可以确定它总是正确的。
159赞 Xeo 8/14/2013
@Oleksiy:想和你说一句话。std::map<std::string, std::vector<std::string>>::const_iterator
11赞 Rapptz 8/14/2013
@Oleksiy 我建议阅读这个 GotW
5赞 mip 5/29/2015
@Xeo通常是更清洁的选择。我只用于本地范围类型。typedef std::map<std::string, std::vector<std::string>> MyContainerauto
25赞 JAB 2/19/2016
@doc我会说更好(特别是因为你可以模板化它!using MyContainer = std::map<std::string, std::vector<std::string>>;

答:

539赞 Oleksiy 8/14/2013 #1

基本上是从Bjarne Stroustrup的“C++编程语言第4版”中复制和粘贴:

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

  • 一个整数不能转换为另一个不能保持其值的整数。例如,char 允许 to int,但不允许 int 到 char。
  • 浮点值不能转换为另一个不能保存其浮点类型的浮点值 价值。例如,允许将浮点数转换为双精度,但不允许将双精度转换为浮点数。
  • 浮点值不能转换为整数类型。
  • 整数值不能转换为浮点类型。

例:

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)

}

= 优于 {} 的唯一情况是使用 keyword 获取初始值设定项确定的类型。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赞 juanchopanza 8/14/2013
还有一个事实是,using 可以解析为函数声明。你可以说但不能说,这是令人困惑和不一致的.而有时候,你确定,你甚至不能说。()T t(x,y,z);T t()xT t(x);
128赞 user1520427 2/2/2015
我强烈不同意这个答案;当您的类型带有 CTOR 接受 .RedXIII提到了这个问题(只是把它刷掉了),而你完全忽略了它。 并且可以调用完全不同的函数,这是一件需要了解的重要事情。它甚至可能导致看似不直观的调用。说你应该默认更喜欢会导致人们误解正在发生的事情。不过,这不是你的错。我个人认为这是一个非常糟糕的功能。std::initializer_listA(5,4)A{5,4}{}
22赞 Oleksiy 2/3/2015
@user1520427这就是为什么有“除非你有充分的理由不这样做”的部分。
87赞 Edoardo Dominici 10/31/2015
虽然这个问题很老,但它有很多命中,因此我在这里添加它只是为了参考(我没有在页面的其他任何地方看到它)。从 C++14 开始,使用新的规则从 braced-init-list 中自动扣除,现在可以编写,并且不会再推导出为 .auto var{ 5 }intstd::initializer_list<int>
72赞 jaques-sam 3/12/2019
哈哈,从所有的评论来看,仍然不清楚该怎么做。显而易见的是,C++规范是一团糟!
121赞 Red XIII 8/14/2013 #2

使用大括号初始化的原因有很多,但您应该知道,initializer_list<>构造函数比其他构造函数更受欢迎,默认构造函数除外。这会导致构造函数和模板出现问题,其中类型构造函数可以是初始值设定项列表,也可以是普通的旧 ctor。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赞 Xeo 8/14/2013
这是泛型编程中非常重要的一点。编写模板时,不要使用 braced-init-lists(标准的名称),除非您想要语义(好吧,也许用于默认构造对象)。{ ... }initializer_list
106赞 user1520427 2/2/2015
老实说,我不明白为什么会有这个规则——它只会给语言增加混乱和混乱。如果你想要构造函数,这样做有什么问题?这似乎比优先于所有其他重载要容易得多。std::initializer_listFoo{{a}}std::initializer_liststd::initializer_list
6赞 Gabriel 4/20/2015
上面的评论+1,因为我认为这真的是一团糟!!这不是逻辑; 对我来说,遵循一些逻辑远远超过它变成初始化程序列表优先级(UPS用户可能会认为嗯......Foo{{a}}Foo{a}
47赞 mip 5/29/2015
基本上,C++11 用另一个混乱替换了一个混乱。哦,对不起,它不会取代它 - 它增加了它。你怎么知道你是否遇到过这样的课程?如果你在没有构造函数的情况下开始,但它将在某个时候被添加到类中以扩展其接口怎么办?然后类的用户就搞砸了。std::initializer_list<Foo>FooFoo
15赞 dwanderson 4/21/2016
..“使用大括号初始化的许多原因”是什么?这个答案指出了一个原因(),它并不能真正确定说它是首选,然后继续提到一个不受欢迎的好案例。我错过了什么,~30 其他人(截至 2016-04-21)觉得有帮助?initializer_list<>
191赞 MikeMB 5/14/2016 #3

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

  • 如果我创建的对象在概念上包含我在构造函数中传递的值(例如容器、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赞 Mikhail 11/27/2016
完全同意你的大部分答案。但是,您不认为为向量放置空括号是多余的吗?我的意思是,当你需要对泛型 T 的对象进行值初始化时,这没关系,但是对非泛型代码这样做的目的是什么?
16赞 MikeMB 11/28/2016
@Mikhail:这当然是多余的,但我的习惯是总是显式地进行局部变量初始化。正如我所写的,这主要是关于一致性的,所以当它确实重要时,我不会忘记它。不过,这当然不是我在代码审查或风格指南中提到的。
7赞 laike9m 3/20/2018
相当干净的规则集。
7赞 UKMonkey 5/9/2018
这是迄今为止最好的答案。{} 就像继承一样——容易被滥用,导致代码难以理解。
6赞 Johannes Schaub - litb 11/19/2018
@MikeMB示例:<- 不会尝试创建未初始化的引用,而是将其绑定到临时整数对象。第二个示例:<- 不会尝试创建未初始化的引用(就像这样做的那样),而是将其绑定到一个临时整数对象,然后让它悬空。GCC 即使有,也不会对第二个示例发出警告。const int &b{}struct A { const int &b; A():b{} {} };()-Wall
6赞 Allan Jensen 10/3/2019 #4

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

注意:

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

这两者结合在一起意味着如果里面的东西是原始常量,它们更安全,但如果它们是对象,它们就不那么安全了(尽管在 C++20 中是固定的)

评论

1赞 Mark Storer 10/23/2019
我尝试在 goldbolt.org 上四处走动,使用提供的示例代码绕过“显式”或“私有”构造函数,并使其中一个或另一个私有或显式,并得到了相应的编译器错误的奖励。想用一些示例代码来支持它吗?
1赞 Allan Jensen 11/10/2019
这是针对 C++20 建议的问题的修复程序:open-std.org/jtc1/sc22/wg21/docs/papers/2018/p1008r1.pdf
1赞 Mark Storer 11/13/2019
如果你编辑你的答案以显示你正在谈论的C++版本,我很乐意改变我的投票。
0赞 TamaMcGlinn 8/31/2021
clang++ -std=c++14告诉我.就隐式调用显式构造函数而言,该参数甚至没有意义。这是一个隐式构造函数调用:。false 通过调用匹配构造函数隐式转换为 Foo。如果使用大括号,则显式调用构造函数。关键是你不能在不提及类型名称的情况下使用大括号进行这样的赋值。main.cpp:22:7: error: calling a private constructor of class 'Foo'foo_instance = false;
10赞 codeling 10/6/2021 #5

更新(2022-02-11): 请注意,关于该主题的最新观点与最初发布的观点(如下)相反,这些观点反对 {} 初始值设定项的偏好,例如 Arthur Dwyer 在他的博客文章中 C++ 中初始化的骑士

原文答案:

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

要点/复制自第 4 节:

何时应使用 ( ) 与 { } 语法来初始化对象?为什么? 以下是简单的指南:

指南:最好使用 { } 的初始化,例如 vector v = { 1, 2, 3, 4 };或者 auto v = vector{ 1, 2, 3, 4 };,因为 它更一致,更正确,并且避免了必须了解 完全是老式的陷阱。在您喜欢的单参数情况下 仅查看 = 符号,例如 int i = 42;和 auto x = 任何东西; 省略大括号是可以的。...

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

...在极少数情况下,例如向量 v(10,20);或者 auto v = vector(10,20);,则使用带有 ( ) 的初始化来显式调用 以其他方式被initializer_list隐藏的构造函数 构造 函数。

但是,这通常应该是“罕见”的原因是因为默认 和副本结构已经很特殊了,并且可以很好地使用 { },并且 好的类设计现在大多避免了诉诸 ( ) 的情况 用户定义的构造函数,因为以下最终设计指南:

指南:在设计类时,请避免提供 模棱两可地重载一个initializer_list构造函数,以便 用户不需要使用 ( ) 来访问这样一个隐藏的构造函数。

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