EMCPP条款7:在创建对象时注意区分()和{}

YiQi 管理员

大括号初始化有一项新特性,就是它禁止内建型别之间进行隐式窄化型别转换(narrowing conversion)。如果大括号内的表达式无法保证使用初始化的对象来表达,则代码不能通过编译。而采用小括号和 = 的初始化则不会进行窄化型别转换检查,因为如果那样的话就会破坏太多的遗留代码了:

1
2
3
4
5
double x, y, z;
...
int sum1{x + y + z}; // 错误:double 到 int 的转换
int sum2(x + y + z); // 正确:表达式被截断为 int
int sum3 = x + y + z; // 正确:表达式被截断为 int

大括号初始化的缺陷在于伴随它有时会出现的意外行为。这种行为源千大括号初始化物、 std::initializer_list 以及构造函数重载决议之间的纠结关系。

在构造函数被调用时,只要形参中没有任何一个具备 std::initializer_list 型别,那么小括号和大括号的意义就没有区别:

1
2
3
4
5
6
7
8
9
10
class Widget {
public:
Widget(int i, bool b); // ctors not declaring std::initializer_list params
Widget(int i, double d);

};
Widget w1(10, true); // calls first ctor
Widget w2{10, true}; // also calls first ctor
Widget w3(10, 5.0); // calls second ctor
Widget w4{10, 5.0}; // also calls second ctor

如果,有一个或多个构造函数声明了任何一个具备 std::initializer_list 型别的形参,那么采用了大括号初始化语法的调用语句会强烈地优先选用带有 std::initializer_list 型别形参的重载版本。

1
2
3
4
5
6
7
8
9
10
11
12
13
class Widget {
public:
Widget(int i, bool b); // as before
Widget(int i, double d); // as before
Widget(std::initializer_list<long double> il); // added

};
Widget w1(10, true); // uses parens and, as before, calls first ctor
Widget w2{10, true}; // uses braces, but now calls std::initializer_list ctor
// (10 and true convert to long double)
Widget w3(10, 5.0); // uses parens and, as before, calls second ctor
Widget w4{10, 5.0}; // uses braces, but now calls std::initializer_list ctor
// (10 and 5.0 convert to long double)

即使是平常会执行复制或移动的构造函数也可能被带有 std::initializer_list 型别形参的构造函数劫持:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Widget {
public:
Widget(int i, bool b); // as before
Widget(int i, double d); // as before
Widget(std::initializer_list<long double> il); // as before
operator float() const; // convert to float

};
Widget w5(w4); // uses parens, calls copy ctor
Widget w6{w4}; // uses braces, calls std::initializer_list ctor
// (w4 converts to float, and float converts to long double)
Widget w7(std::move(w4)); // uses parens, calls move ctor
Widget w8{std::move(w4)}; // uses braces, calls std::initializer_list ctor
// (for same reason as w6)

编译器的决心如此强烈,比如下面代码

1
2
3
4
5
6
7
8
class Widget {
public:
Widget(int i, bool b); // as before
Widget(int i, double d); // as before
Widget(std::initializer_list<bool> il); // element type is now bool
// no implicit conversion funcs
};
Widget w{10, 5.0}; // error! requires narrowing conversions

这里,编译器会忽略前两个构造函数(第二个构造函数的形参表和实参表的型别是精确匹配的),转而尝试带有一个 std::initializer_list<bool> 型别形参的构造函数。

只有在找不到任何办法把大括号初始化物中的实参转换成 std::initializer_list 模板中的型别时,编译器才会退而去检查普通的重载决议

1
2
3
4
5
6
7
8
9
10
11
12
class Widget {
public:
Widget(int i, bool b); // as before
Widget(int i, double d); // as before
// std::initializer_list element type is now std::string
Widget(std::initializer_list<std::string> il);
// no implicit conversion funcs
};
Widget w1(10, true); // uses parens, still calls first ctor
Widget w2{10, true}; // uses braces, now calls first ctor
Widget w3(10, 5.0); // uses parens, still calls second ctor
Widget w4{10, 5.0}; // uses braces, now calls second ctor

假定你使用了一对空大括号来构造一个对象,而该对象既支持默认构造函数,又支持带有 std::initializer_list 型别形参的构造函数。那么,这对空大括号的意义是什么呢?语言规定,在这种情形下应该执行默认构造。

1
2
3
4
5
6
7
8
9
class Widget {
public:
Widget(); // default ctor
Widget(std::initializer_list<int> il); // std::initializer_list ctor
// no implicit conversion funcs
};
Widget w1; // calls default ctor
Widget w2{}; // also calls default ctor
Widget w3(); // most vexing parse! declares a function!

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
2
Widget w4({}); // calls std::initializer_list ctor with empty list 
Widget w5{{}}; // ditto

对于 std::vector

1
2
3
4
std::vector<int> v1(10, 20); // use non-std::initializer_list ctor: create 10-element
// std::vector, all elements have value of 20
std::vector<int> v2{10, 20}; // use std::initializer_list ctor: create 2-element std::vector,
// element values are 10 and 20

作为结论,你最好把构造函数设计成客户无论使用小括号还是大括号都不会影响调用的重载版本才好。换言之,现在一般是把 std::vector 的接口设计视为败笔的,应该从中汲取教训,避免同类行为。

要点速记

  • 大括号初始化可以应用的语境最为宽泛,可以阻止隐式窄化型别转换,还对最令人苦恼之解析语法免疫。
  • 在构造函数重载决议期间,只要有任何可能,大括号初始化物就会与带有 std::initializer_list 型别的形参相匹配,即使其他重载版本有着貌似更加匹配的形参表。
  • 使用小括号还是大括号,会造成结果大相径庭的一个例子是:使用两个实参来创建一个 std::vector<数值型别> 对象 。
  • 在模板内容进行对象创这时,到底应该使用小括号还是大括号会成为一个棘手问题。
此页目录
EMCPP条款7:在创建对象时注意区分()和{}