今天学习了《Effective Modern C++》Item 7 Distinguish () and {} when creating objects. 为了防止以后遗忘,在这里总结一下。
C++11 标准中,有三种对象的初始化方式,分别为 ()、{}、= 。代码如下所示:
int x(0); // way 1. initializer is in parentheses有些情况下 ,也可用等号(=)和大括号({})结合来初始化对象。例如:
int y = 0; // way 2. initializer follows "="
int z{0}; // way 3. initializer is in braces
int z = {0}; // way 4. initializer uses "=" and braces在这里,我们将way3 和way4合并作为同一种初始化方式,即用大括号的方式进行初始化。
在以上3中对象创建方式中,使用大括号的方式是适用性最高的方式,其他两种方式则在某些使用情境下会产生编译错误,比如:
在类的定义中,就不能用小括号"()"的方法初始化成员变量:
class Widget
{
public:
Widget() {};
private:
int a{0}; // fine. a's default value is 0.
int b = 10; // fine. b's default value is 10.
int c(20); // error!
};
另一方面,对于那些“uncopyable”的对象,则不能使用“=”初始化。
std::atomic<int> ai1{0}; // fine
std::atomic<int> ai2(0); // fine
std::atomic<int> ai3 = 0; // error
所以,在三种初始化对象的方式中,只有“大括号”是最为通用的。那么,我们在使用大括号的方式进行对象初始化时要注意些什么呢?或者大括号初始化方式有些什么特点呢?
特点1:阻止内置类型间的缩小转换(narrowing conversation)。
缩小转换是指将一个对象从表示范围大的类型(如double)转换为表示范围小的类型(如int),大括号初始化可以阻止内置类型间的这种转换。
double x, y, z;
int sum1{x + y + z}; // error! sum of doubles may not be expressible as int
特点2:避免类对象定义中二义性的产生。
假设有一个名字叫做“Widget”的类,有如下类对象的定义:
Widget w1(10); // 很好,调用Widget的有一个整形参数的构造函数
Widget w2(); //产生二义性,是调用Widget的默认无参构造函数?还是生命了一个名字为w2,返回类型为Widget的无参函数?
如果改为用大括号来初始化对象,就不会产生二义性了:
Widget w1{10}; // 很好,像以前一样,调用Widget中有一个参数的那个构造函数
Widget w2{}; // 很好,调用Widget的无参构造函数
以上两个特点,算是大括号初始化方式给我们带来的惊喜,但伴随着这些惊喜,大括号初始化方式也带来一些令人不那么愉快的副作用。
副作用1:对auto类型推导的影响。
当auto遇上大括号时,类型推导机制会将变量推导为std::initializer_list<T>类型:
auto v1 = -1; // -1 是int类型, 所以v1是int类型
auto v2(-1); // -1 是int类型, 所以v2是int类型
auto v3{-1}; // -1 仍然是int, 但 v3的类型是 std::initializer_list<int>
auto v4 = {-1}; // -1仍然是int, 但v4's的类型是 std::initializer_list<int>
副作用2:对类构造函数的影响。
如果用大括号的方式进行类对象的初始化,编译器会选择含有std::initializer_list<>类型参数的构造函数,即使那些不含std::initializer_list的构造函数更加匹配。
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对象定义如下:
Widget w1(10, true); //调用第一个构造函数在上面的例子中,w2和w4使用大括号方式初始化,尽管第一个、第二个构造函数的参数最能匹配w2和w4的定义,但是编译器仍然会选择第三个构造函数来初始化w2和w4.
Widget w2{10, true}; //大括号初始化, 调用第三个构造函数
Widget w3(10, 5.0); //小括号初始化,调用第二个构造函数
Widget w4{10, 5.0}; //使用大括号初始化,调用第三个构造函数
如果将Widget的第三个构造函数改为:
Widget(std::initializer_list<bool> il);
编译器仍然为会为w4调用第三个构造函数,但是注意,这时会发生缩小转换,这是大括号初始化方式所禁止的,所以下面的声明会报错:
Widget w{10, 5.0}; // error! requires narrowing conversions
那么问题来了,如果用户既想用大括号初始化方式定义对象,又不想让编译器匹配第三个构造函数,应该怎么办呢?以本文中的Widget为例,可以将第三个构造函数改为如下形式:
// std::init_list element type is now std::string
Widget(std::initializer_list<std::string> il);
此时,变量定义及调用的构造函数如下:
Widget w1(10, true); // 使用小括号, 仍然调用第一个构造函数
Widget w2{10, true}; // 使用大括号,但是不存在从(int bool)到string的转换,所以仍然匹配第一个构造函数
Widget w3(10, 5.0); // 使用小括号, 匹配第二个构造函数
Widget w4{10, 5.0}; // 使用大括号,但是不存在从(int double)到string的转换,所以仍然匹配第二个构造函数
通过上面的例子,编译器为大括号初始化方式匹配第三个构造函数(形参为std::initializer_list<T>)的前提是存在实参到T的类型转换。
两个例外:
在用大括号进行初始化的时候有两个例外:
例外1. 空大括号意味着无参,而不是空的std::initializer_list. 在对象的定义中会调用类的无参构造函数。
例如有如下类定义:
class Widget {
public:
Widget(); //默认构造函数
Widget(std::initializer_list<int> il);
… //
};
有如下对象定义
Widget w1; // 调用默认构造函数
Widget w2{}; // 仍然调用默认构造函数,而不是创建空的 std::initializer_list
Widget w3(); // 二义性,定义了一个函数
如果想让编译器匹配第二个构造函数,则对象定义格式应如下所示:
Widget w4({}); // 调用第二个构造函数
w5{{}}; // 调用第二个构造函数例外2 大括号初始化方式不影响拷贝和移动构造函数 :
设有类定义如下:
class Widget {
public:
Widget(const Widget& rhs); // 1 copy constructor
Widget(Widget&& rhs); // move constructor
Widget(std::initializer_list<int> il); // std::init_list constructor
operator int() const; // convert to int
…
};
auto w6{w5} //调用拷贝构造函数,而不是std::initializer_list 构造函数
auto w7{std::move(w5)}; //调用move constructor