EMCPP条款7:在创建对象时注意区分()和{}
大括号初始化有一项新特性,就是它禁止内建型别之间进行隐式窄化型别转换(narrowing conversion)。如果大括号内的表达式无法保证使用初始化的对象来表达,则代码不能通过编译。而采用小括号和 =
的初始化则不会进行窄化型别转换检查,因为如果那样的话就会破坏太多的遗留代码了:
1 | double x, y, z; |
大括号初始化的缺陷在于伴随它有时会出现的意外行为。这种行为源千大括号初始化物、 std::initializer_list
以及构造函数重载决议之间的纠结关系。
在构造函数被调用时,只要形参中没有任何一个具备 std::initializer_list
型别,那么小括号和大括号的意义就没有区别:
1 | class Widget { |
如果,有一个或多个构造函数声明了任何一个具备 std::initializer_list
型别的形参,那么采用了大括号初始化语法的调用语句会强烈地优先选用带有 std::initializer_list
型别形参的重载版本。
1 | class Widget { |
即使是平常会执行复制或移动的构造函数也可能被带有 std::initializer_list
型别形参的构造函数劫持:
1 | class Widget { |
编译器的决心如此强烈,比如下面代码
1 | class Widget { |
这里,编译器会忽略前两个构造函数(第二个构造函数的形参表和实参表的型别是精确匹配的),转而尝试带有一个 std::initializer_list<bool>
型别形参的构造函数。
只有在找不到任何办法把大括号初始化物中的实参转换成 std::initializer_list
模板中的型别时,编译器才会退而去检查普通的重载决议
1 | class Widget { |
假定你使用了一对空大括号来构造一个对象,而该对象既支持默认构造函数,又支持带有 std::initializer_list
型别形参的构造函数。那么,这对空大括号的意义是什么呢?语言规定,在这种情形下应该执行默认构造。
1 | class Widget { |
If you want to call a std::initializer_list
constructor with an empty std::initializer_list
, you do it by making the empty braces a constructor argument—by putting the empty braces inside the parentheses or braces demarcating what you’re passing:
1 | Widget w4({}); // calls std::initializer_list ctor with empty list |
对于 std::vector
等
1 | std::vector<int> v1(10, 20); // use non-std::initializer_list ctor: create 10-element |
作为结论,你最好把构造函数设计成客户无论使用小括号还是大括号都不会影响调用的重载版本才好。换言之,现在一般是把 std::vector
的接口设计视为败笔的,应该从中汲取教训,避免同类行为。
要点速记
- 大括号初始化可以应用的语境最为宽泛,可以阻止隐式窄化型别转换,还对最令人苦恼之解析语法免疫。
- 在构造函数重载决议期间,只要有任何可能,大括号初始化物就会与带有
std::initializer_list
型别的形参相匹配,即使其他重载版本有着貌似更加匹配的形参表。 - 使用小括号还是大括号,会造成结果大相径庭的一个例子是:使用两个实参来创建一个
std::vector<数值型别>
对象 。 - 在模板内容进行对象创这时,到底应该使用小括号还是大括号会成为一个棘手问题。