- C++11中有四种初始化方法:
int x(0); // 小括号 (parentheses) 初始化
int y = 0; // 等号初始化
int z{ 0 }; // 大括号 (braces) 初始化
int z = { 0 }; // 1、3结合
- 其中方法4基本被视为与方法3相同,以下讨论中忽略。
- C++98中的老规矩:声明时不加任何括号等号,调用默认构造函数(default constructor);声明时用等号或括号,调用复制构造函数(copy constructor);非声明时用等号,调用赋值运算符函数(operator=)。
Widget w1; // call default constructor
Widget w2 = w1; // not an assignment; calls copy ctor (易错点)
w1 = w2; // an assignment; calls copy operator=
- C++98语法中表达不出一些语义:例如,将一个STL容器初始化为一些数值的集合(笔者:无比真实,大一早期写代码痛点之一)。为了解决存在多种初始化方法,每种又不能全部适用的问题,C++11提出了统一初始化(uniform initialization)。统一初始化是表达它可以用在任何地方表达任何事的概念;大括号初始化(braced initialization) 是表达它使用大括号的语法结构。使用它可以表达上面的语义:
std::vector<int> v{ 1, 3, 5 }; // v's initial content is 1, 3, 5
- 大括号和等号初始化一样可以用于在类中初始化非静态成员变量(小括号不行):
class Widget {
private:
int x{ 0 }; // fine, x's default value is 0
int y = 0; // also fine
int z(0); // error!
};
- 大括号和小括号初始化一样可以用于初始化不可复制的对象(等号不行):
std::atomic<int> ai1{ 0 }; // fine
std::atomic<int> ai2(0); // fine
std::atomic<int> ai3 = 0; // error!
- 大括号的另一新特性:阻止内建类型的隐式的缩窄转换(narrowing conversion)。俗话说就是禁止了double转float,long转int,int转char这样,会直接报编译错误。而小括号和等号都是允许这样的隐式转换的。
double x, y, z;
int sum1{ x + y + z }; // error! sum of doubles may
// not be expressible as int
int sum2(x + y + z); // okay (value of expression
// truncated to an int)
int sum3 = x + y + z; // ditto
- 大括号初始化的问题:通常与
std::initializer_list
以及构造函数重载的解析相关,关于auto
推导产生的问题已经Item 2中有所说明。调用构造函数时,如果没有重载含std::initializer_list
参数的构造函数,那么大括号和小括号初始化的意义相同。但一旦出现,就会严重偏向于那样的版本:只要有任何方式能调用std::initializer_list
参数版本的构造函数,编译器就会那样做。甚至复制和移动构造函数都会被“劫持”:
class Widget {
public:
Widget(int i, bool b) {
cout << "int bool ctor" << endl;
}
Widget(int i, double d) {
cout << "int double ctor" << endl;
}
Widget(const Widget& w) {
cout << "copy ctor" << endl;
}
Widget(Widget&& w) {
cout << "move ctor" << endl;
}
Widget(std::initializer_list<long double> il) {
cout << "ctor ini_list" << endl;
}
operator float() const {
cout << "convert to float" << endl;
return 1.f;
}
};
int main()
{
Widget w1(10, true);
Widget w2{ 10, true };
Widget w3(10, 5.0);
Widget w4{ 10, 5.0 };
Widget w5(w4);
Widget w6{ w4 }; // 先由operator float()转为float再调用initializer_list版本
Widget w7(std::move(w4));
Widget w8(std::move(w5));
return 0;
}
加入最后一个构造函数前:
注:这里笔者观察到了奇特的现象:加入initializer_list参数构造函数后,w2和w4的确都会调用该版本,但w6和w8,也就是复制和移动构造,VS和GCC的版本运行结果居然不同:GCC如作者所说调用该版本,而VS调用了原来的复制和移动版本,只有当明确对w4和w5调用 operator float()
转换后才会调用initializer_list版本。如有了解的朋友欢迎在评论区交流:)
VS 运行结果
GCC 运行结果
- 甚至当initializer_list版本的构造函数转换失败时,编译器都选择报错而不是调用参数类型完全符合的其他版本构造函数:
Widget(int i, double d) {
cout << "int double ctor" << endl;
}
Widget(std::initializer_list<bool> il) {
cout << "ctor ini_list" << endl;
}
Widget w{10, 5.0}; // 直接报错:不能缩窄转换(double, int向bool)
- 只有当大括号内的元素完全不可能转换成
std::initializer
模板的类型时,编译器才会重新调用原来的构造函数:
Widget(int i, bool b) {
cout << "int bool ctor" << endl;
}
Widget(int i, double d) {
cout << "int double ctor" << endl;
}
Widget(std::initializer_list<std::string> il) {
cout << "ctor ini_list" << endl;
}
Widget w{10, true}; // 调用第一个构造函数
Widget w{10, 5.0}; // 调用第二个构造函数
- 还有一个有趣的edge case。假设你用了一个空的大括号,那么调用的是默认构造函数还是内容为空的
std::initializer_list
参数构造函数?答案是前者。如果你想调用后者,则应该在大括号内再加一个空的大括号,代表空的std::initializer_list
。当然此时你也可以用小括号来调用该函数。
- 关于大括号初始的语法到这里就差不多了。从这段讨论中,我们应该从这段讨论中吸收两件事:
- 作为一个类的编写者,你要意识到如果给一个原来没有
std::initializer_list
初始化函数的类添加一个,可能导致客户端的调用被大幅度改变,因此一定要谨慎。当前std::vector
的设计就被认为是存在错误的(例如传入两个数10和20,小括号调用创建的是20个值为10的元素,而大括号调用创建的是2个元素分别为值10和20)。 - 作为一个类的使用者,你必须在创建对象时谨慎选择使用小括号还是大括号。二者没有绝对的优劣,重点是你自己要选择一个,保持一致性,并且时刻有着清晰的思路。
- 作为一个类的编写者,你要意识到如果给一个原来没有
总结
- 大括号初始化是能使用场景最广的初始化语法,防止了隐式的缩窄转换。
- 在重载的构造函数选择中,大括号初始化极度倾向于调用
std::initializer_list
参数版本的构造函数。 - 小括号和大括号初始化差异巨大的一个例子是用两个参数创建
std::vector
。