Why is list initialization (using curly braces) better than the alternatives?
1 2 3 4
| MyClass a1 {a}; // clearer and less error-prone than the other three
MyClass a2 = {a};
MyClass a3 = a;
MyClass a4(a); |
为什么?
我找不到答案,所以让我自己回答我的问题。
- 为什么不使用auto?
- @马克加西亚?请详细说明:)
- 不适合注释框;)。无论如何,引用链接文章的话:"…使用auto声明变量的主要原因是为了正确性、性能、可维护性和健壮性,是的,方便性……"。
- 这是真的,它很方便,但在我看来它降低了可读性——我喜欢在读取代码时看到对象是什么类型。如果你100%确定对象是什么类型,为什么使用自动?如果您使用列表初始化(阅读我的答案),您可以确保它总是正确的。
- @奥利克西:埃多克斯1〔1〕想和你谈谈。
- @Oleksiy我建议你读一下这篇文章。
- @msalters:嗯,没有auto,你已经可以做到了。当然,这可能只是一个"有趣的不应该被事实阻止的刺拳"。;)
- @Xeo-typedef std::map> MyContainer通常是更清洁的选择。我只对本地作用域类型使用auto。
- @医生,我想说using MyContainer = std::map>;更好(特别是当你可以模板它!)
- 第一个和第二个不完全相等。第一个是直接列表初始化,第二个是复制列表初始化。参见en.cppreference.com/w/cpp/language/list_初始化
- 可能重复:stackoverflow.com/q/9976927/3560202?
- 另请参见isocpp.github.io/cppcore指南/…
- 只是后来的一个补充:"几乎总是自动的"制造了一个圣杯-它不是!!!!.使用auto(上面的迭代器示例)有很好的理由,但在其他情况下,最好不要使用auto。实例:
- unsigned int n = 7;与auto n = 7U;的比较,仅仅是为了容易忘记后缀(出于演示目的,故意在第一个变体中实际发生的情况,但是,第一个变体具有很强的抗干扰性),导致n的类型不好。更糟糕的是:auto n = 7UL;如果你想强制n是uint64_t和bam类型,我们不在64位Linux上,只需要得到uint32_t(或者甚至可能不是这个,因为uint32_t可能在当前系统上被定义为unsigned int,你可能最终会得到不兼容的指针)。
- 上帝,只在C++中做这样的问题甚至存在。谢谢你问这个问题,答案真的很有帮助
基本上从Bjarne Stroustrup的《C++程序设计语言第四版》中复制和粘贴:
列表初始化不允许缩小范围(§ISO.8.5.4)。即:
- 一个整数不能转换为另一个不能保存其值的整数。例如,char允许to int,但不允许int to char。
- 不能将浮点值转换为另一个不能保存其值的浮点类型价值。例如,允许float to double,但不允许double to float。
- 浮点值无法转换为整数类型。
- 整数值无法转换为浮点类型。
例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| 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关键字获取初始值设定项确定的类型时,才会优先使用=而不是。
例子:
1 2
| auto z1 {99}; // z1 is an initializer_list<int>
auto z2 = 99; // z2 is an int |
结论
首选初始化而不是替代方案,除非您有充分的理由不这样做。
- 还有一个事实是,使用()可以解析为函数声明。你可以说T t(x,y,z);,但不能说T t(),这是令人困惑和不一致的。有时,你确定的是x,你甚至不能说T t(x);。
- 我强烈反对这个答案;当您有类型和接受std::initializer_list的ctor时,有支撑的初始化会变得完全混乱。Redxiii提到了这个问题(只是擦掉了它),而你完全忽略了它。A(5,4)和A{5,4}可以调用完全不同的函数,这是需要知道的重要内容。它甚至可能导致看起来不具说服力的呼叫。如果说你默认情况下更喜欢{},会导致人们误解正在发生的事情。不过,这不是你的错。我个人认为这是一个考虑得非常糟糕的功能。
- @用户1520427这就是为什么有"除非你有充分的理由不"的部分。
- @Oleksiy我认为考虑替代方案总是有很强的理由的,特别是当简单地添加一个std::initializer_list构造函数可以改变现有{}调用的语义时。我真的不明白你是怎么得出这样的结论的,但也许那只是因为我认为语言的这一部分需要付出更多的努力。哦,好吧。
- @用户1520427我明白您的意思,但在本例中,接受std::initializer_list的构造函数是考虑备选方案的唯一原因。在其他任何情况下,最好使用{}初始化。如果我不明白,请告诉我。
- 虽然这个问题很老,但它有很多点击率,因此我在这里添加这个仅供参考(我在页面上的其他地方没有看到)。从C++ 14,用新的规则从支持的init列表中自动推导,现在可以编写EDCOX1,13,并且它将被推导为EDCOX1,14,不作为EDCOX1,15。
- 现在用auto的例子是不正确的。应改为:z1为int,z2为int,auto z3 = {99}; // z3 is an std::initializer_list。
- @Juanchopanza,这是最麻烦的解析问题,对吧?或者还有其他可能发生这种情况的情况吗?
- @是的,就是这样。
- @Oleksiy对于字符串,字符串s1("hello");和字符串s2"hello"之间的用法有什么不同,哪一个更好?
- 我认为这不能解释为什么它更好。不仅您假定的问题是这样加载的,而且答案只说明了类型的初始化转换是如何工作的;这并不比调用函数时所做的转换更安全。更糟的一个完美例子是struct X{int i; int j}; X {5};,它将编译;但不将j初始化。
- 编辑j默认构造-但是如果需要填充j以使类有意义,那么一切都会出错。
- @ EdoardoSparkonDominici,这种变化并没有把它变成C++ 14;它一直持续到C++ 17被添加。
- "永远从桥上跳下去,除非你有强烈的理由不这样做"@user我这样解释它。
- @juanchopanza列表初始化有其自身的缺点。你可以说T &t{otherT},但不能说T &t{otherT, somethingElse},但是你可以说const T& t{otherT}和const T& t{otherT, somethingElse}。但是在第一种情况下,&t == &otherT,但在第二种情况下,有一个临时的创建。旧的初始化风格一贯地防范这种危险,只会使多参数变量格式错误。
- 哈哈,从所有的评论来看,现在还不清楚该怎么做。很明显的是,C++规范是一团糟!
使用大括号初始化有很多原因,但您应该注意,initializer_list<>构造函数优先于其他构造函数,异常是默认构造函数。这会导致构造函数和模板出现问题,其中类型T构造函数可以是初始值设定项列表或普通的旧ctor。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| 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!!!
} |
假设您没有遇到这样的类,那么没有理由不使用初始化器列表。
- 这是通用编程中非常重要的一点。当您编写模板时,不要使用有支撑的初始化列表(标准名称用于{ ... }),除非您需要initializer_list语义(好的,并且可能用于默认构造对象)。
- 老实说,我不明白为什么std::initializer_list规则甚至存在——它只是增加了语言的混乱和混乱。如果需要std::initializer_list构造函数,那么执行Foo{{a}}有什么问题?这似乎比让std::initializer_list优先于所有其他重载更容易理解。
- +1对于上面的评论,因为我觉得这真是一团糟!!这不是逻辑;Foo{{a}}对我来说比Foo{a}更符合某种逻辑,而Foo{a}变成了初始化列表优先级(UPS可能会让用户认为是hm…)
- 基本上C++ 11用另一个烂摊子来代替一个烂摊子。哦,对不起,它没有取代它-它增加了。你怎么知道你没有遇到这样的课程?如果您开始时没有使用std::initializer_list构造函数,但在某一点上它将被添加到Foo类中以扩展其接口,该怎么办?那么,Foo类的用户就搞砸了。
- Foo c {a}从cwg 1467起只调用copy ctor(至少在clang中实现)
- …什么是"使用大括号初始化的许多原因"?这个答案指出了一个原因(initializer_list<>),谁说它是首选,这是不真正符合条件的,然后继续提到一个不首选的好案例。我错过了什么?30个其他人(截至2016年4月21日)发现有帮助?
- 这些"许多原因"是什么?
- @Doc:对于任何函数都可以这样说:"如果以后添加更好的匹配重载呢?"
关于使用列表初始化的好处已经有了很好的答案,但是我个人的经验法则是,尽可能不要使用大括号,而是让它依赖于概念意义:
- 如果我在概念上创建的对象持有我在构造函数中传递的值(例如容器、pod结构、原子、智能指针等),那么我将使用大括号。
- 如果构造函数类似于普通函数调用(它执行一些或多或少复杂的操作,这些操作由参数参数参数化),那么我使用的是普通函数调用语法。
- 对于默认初始化,我总是使用大括号。首先,通过这种方式,不管对象是一个"真实"类,它都会被初始化,其中包含一个默认的构造函数,可以无论如何调用它,或者是一个内置的/pod类型。其次,在大多数情况下,它与第一个规则一致,因为默认的初始化对象通常表示一个"空"对象。
根据我的经验,在默认情况下,这个规则集可以比使用大括号更加一致地应用,但是当不能使用它们或它们的含义与带括号的"normal"函数调用语法(调用不同的重载)不同时,必须显式地记住所有异常。
例如,它非常适合标准库类型,如std::vector:
1 2 3 4 5 6 7
| vector<int> a{10,20}; //Curly braces -> fills the vector with the arguments
vector<int> b(10,20); //Parenthesis -> 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 |
- 完全同意你大部分的回答。但是,您不认为为向量加上空括号是多余的吗?我的意思是,当您需要值初始化一个t类泛型对象时,这是可以的,但是对于非泛型代码,这样做的目的是什么呢?
- @米哈伊尔:这当然是多余的,但我的习惯是总是让局部变量初始化显式化。正如我写的,这主要是关于一致性,所以我不会忘记,当它确实重要的时候。当然,我不会在代码评审中提到它,也不会把它放在样式指南中。
- 非常干净的规则集。
- 这是迄今为止最好的答案。就像继承-容易被滥用,导致代码难以理解。
- "首先,我总是确信对象是被初始化的。"不幸的是,对于引用,这不是真的。但在这种情况下,大括号init可能会悄悄地创建对临时文件的悬空引用。
- @约翰内斯肖布:对不起,我不太明白你的意思。如何创建UnitInitialized引用(不应该创建compileTime错误吗?)?由于这部分是关于没有任何参数的构造(是的,默认初始化可能不是正确的术语,但我放弃了对不同类型的初始化的分类),我也不理解这应该如何创建悬空引用。
- @Mikemb示例:const int &b{}不尝试创建未初始化的引用,而是将其绑定到临时整数对象。第二个例子:struct A { const int &b; A():b{} {} };不试图创建一个未初始化的引用(就像()所做的那样),而是将其绑定到一个临时整数对象,然后让它悬空。GCC即使使用-Wall也没有警告第二个例子。
- @约翰内斯肖布·利特。该死,完全忘记了,但我仍然很难想象这是个问题。第二个示例的构造函数显然是被破坏的(对于某些明显的定义),所以它是类设计中的一个bug,而不是指南中的一个bug(clang甚至给出了一个错误)。如果省略了构造函数(使其成为pod),那么在实例化时会得到一个错误。第一个例子不应该是一个问题,因为引用应该延长临时的生命周期-否?
- 我个人甚至更进一步:std::vector v({10, 12});…