Effective Modern C++ Item 7 总结:关于两种对象创建方法“()、{}”的区分

时间:2022-02-19 19:31:51

今天学习了《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);   //调用第一个构造函数
Widget w2{10, true}; //大括号初始化, 调用第三个构造函数

Widget w3(10, 5.0); //小括号初始化,调用第二个构造函数
Widget w4{10, 5.0}; //使用大括号初始化,调用第三个构造函数
在上面的例子中,w2和w4使用大括号方式初始化,尽管第一个、第二个构造函数的参数最能匹配w2和w4的定义,但是编译器仍然会选择第三个构造函数来初始化w2和w4.

如果将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