- 大括号初始化可以应用的语境最为宽泛。可以避免令人苦恼的解析语法、可以阻止隐式窄化型别转换
- 构造函数重载决议期间,只要有任何可能,大括号初始化物就会与带有std::initializer_list型别的形参想匹配,即使其他重载版本更合适
- 使用两个实参来创建std::vector<数值类型>结果会大相径庭。这是大括号与小括号之间的一个明显不同的例子
我们指定初始化的方式包括使用小括号、使用等号或者使用大括号。
int x(0); //使用小括号初始化
int y = 0; //使用等号初始化
int z{
0}; //使用大括号初始化
int z = {
0}; //等号加大括号的用法等同于使用大括号
很多人喜欢使用等号来书写初始化语句,新手往往会认为这里面会发生一次赋值,但是实际上是没有的。我们使用int等内建型别不需要区分的这么开,但是当我们使用自定义型别的时候,就必须区分开初始化和赋值的概念了。
Widget w1; //调用的是默认构造函数
Widget w2 = w1; //是初始化而并非赋值,调用的是复制构造函数
w1 = w2; //并非赋值,调用的是复制赋值运算符
C++11中引入了统一初始化:单一的、至少从概念上可以用于一切场合、表达一切意思的初始化。它的基础是大括号形式。
大括号初始化可以表达之前无法表达之事。
使用大括号来指定容器的初始内容非常简单:
std::vector<int> v{
1,3,5}; //v的初始内容为1,3,5
大括号初始化可以为非静态成员指定默认初始化值,C++11中也可以使用“=”初始化语法,但是不能使用小括号:
class Widget{
private:
int x{
0};
int y = 0;
//int z(0); //错误
};
不可复制的对象可以采用大括号和小括号来初始化,但是不能使用“=”
std::atomic<int> ai1{
0};
std::atomic<int> ai2(0);
//std::atomic ai3 = 0; //错误
通过上述情况我们可以看到,这三种初始化方法中,只有大括号初始化方法适用所有场合。
不过大括号初始化有一项新特性,就是它禁止内建型别之间进行隐式窄化型别转换。如果大括号内的表达式无法保证能够采用进行初始化的对象来表达,则代码不能通过编译,不过小括号和等号可以:
double x,y,z;
//int sum{x + y + z}; //错误,double型别之和可能无法通过int表达
int sum2(x + y + z);
int sum3 = x+y+z;
大括号初始化的一项特征是,它对于C++的最令人苦恼的解析语法免疫。C++规定:任何能够解析为声明的都要解析为声明,而这会带来副作用。程序员本来想要以默认方式构造一个对象,结果却不小心声明了一个函数。这个错误的根本原因在于构造函数调用语法。
//以传递参数方式调用构造函数
Widget w1(10); //调用Widget的构造函数,传入形参10
//如果调用一个没有形参的Widget构造函数的话,结果却变成了声明一个函数而非对象
Widget w2(); //声明了一个名为w2,返回一个Widget型别对象的函数
由于函数声明不能使用大括号来指定形参列表,所以使用大括号来完成对象的默认构造没有这个问题:
Widget w3{
}; //调用没有形参的构造函数
大括号初始化存在一些缺陷,伴随大括号初始化有时会出现意外行为。这种行为源于大括号初始化物、std::initializer_list以及构造函数重载决议之间的纠结关系。这几者之间的相互作用可以使代码看起来要做某一件事,实际上却在做另一件事。比如使用大括号初始化物来初始化一个使用auto声明的变量,那么推导出来的型别就会变成std::initializer_list。
在构造函数被调用时,只要形参中没有任何一个具备std::initializer_list型别,那么小括号和大括号的意义就没有区别。如果一个或多个构造函数声明了任何一个具备std::initializer_list型别的形参,那么采用大括号初始化语法的调用语句会优先选用带有std::initializer_list型别形参的重载版本。即使是平常会执行复制或移动的构造函数也会被带有std::initializer_list型别形参的构造函数劫持:
//构造函数没有std::initializer_list型别,大括号和小括号初始化没有区别
class Widget{
public:
Widget(int i,bool b);
Widget(int i,double d);
};
Widget w1(10,true); //调用第一个构造函数
Widget w2{
10,true}; //调用第一个构造函数
Widget w3(10,5.0); //调用第二个构造函数
Widget w4{
10,5.0}; //调用第二个构造函数
//构造函数一旦有std::initializer_list形参,那么大括号初始化一定会选用这个构造函数
class Widget{
public:
Widget(int i,bool b);
Widget(int i,double d);
Widget(std::intializer_list<long double> il);
operator float() const ; //强制转换成float型别
};
Widget w1(10,true); //调用第一个构造函数
Widget w2{
10,true}; //使用大括号,调用第三个构造函数,10和true被强制转换为long double
Widget w3(10,5.0); //调用第二个构造函数
Widget w4{
10,5.0}; //使用大括号,调用第三个构造函数,10和5.0被强制转换为long double
Widget w5(w4); //使用小括号,调用的是复制构造函数
Widget w6{
w4}; //使用大括号,调用第三个构造函数
//w4的返回值被强制转换float,而float又被强制转换为long double
Widget w7(std::move(w4)); //使用小括号,调用的是移动构造函数
Widget w8{
std::move(w4)}; //使用大括号,调用第三个构造,和w6结果相同
这种优先调用是很强烈的,即使最优选的调用std::initializer_list的构造函数无法被调用,编译器还是会选择这个。只有在找不到任何办法把大括号初始化物中的实参转化成std::initializer_list模板中的型别时,编译器才会去检查普通的重载函数。
class Widget{
public:
Widget(int i,bool b);
Widget(int i,double d);
Widget(std::initializer_list<bool> il);
};
Widget w{
10,5.0}; //无法通过编译,无法把10和5.0窄化为bool
class Widget{
public:
Widget(int i,bool b);
Widget(int i,double d);
Widget(std::initializer_list<std::string> il);
};
Widget w{
10,5.0}; //调用第二个构造函数,因为编译器无法把int和double转换成string类型,所以寻找重载函数
对于std::initializer_list还有个小问题,当我们使用一对空大括号来构造一个对象,而该对象既支持默认的构造,也支持带有std::initializer_list型别参数的构造。此时的这对空大括号的意思是“没有实参”而不是“空的std::initializer_list”。如果我们想要传入一个空的std::initializer_list,可以通过把空大括号作为构造函数实参的方式实现,即把一对空大括号放入一对小括号或大括号。
class Widget{
public:
Widget(); //默认构造
Widget(std::initializer_list<int> il);
};
Widget w1; //调用默认构造
Widget w2{
}; //调用默认构造
Widget w3(); //解析语法,变成函数声明而不是调用构造
Widget w4({
}); //调用带有std::initializer_list参数的构造,传入空的std::initializer_list
Widget w5{
{
}}; //同上
大括号初始化物、std::initializer_list、构造函数重载决议,这些内容不注意会有很大的影响。直接影响到的就是std::vector类。std::vector类中有一个形参中没有std::initializer_list型别的构造函数,它允许我们指定容器的初始尺寸,以及一个初始化时让所有元素拥有的值。但是它还有一个带有一个std::initializer_list型别形参的构造函数,允许我们逐个指定容器中的元素值。如果我们要创建一个元素为数值型别的std::vector,并传递了两个实参给构造函数的话,用小括号还是大括号结果会大相径庭:
//小括号,调用了形参中没有一个具备std::initializer_list型别的构造函数
//结果是创建了一个含有10个元素的std::vector,所有的元素值都是20
std::vector<int> v1(10,20);
//大括号,调用了形参中含有std::initializer_list型别的构造函数
//结果是创建了一个含有2个元素的std::vector,元素的值分别为10和20
std::vector<int> v2{
10,20};
所以从某种程度上说vector的设计是有缺陷的。我们自己在设计一个类的时候,我们需要意识到自己撰写的一组重载构造函数中只要有std::initializer_list形参,则使用大括号初始化的客户代码只会发现这些构造的重载版本。