Update item7.md

This commit is contained in:
猫耳堀川雷鼓 2021-03-02 12:10:56 +08:00 committed by GitHub
parent caad38385d
commit f25c878636
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

View File

@ -10,15 +10,15 @@
从你的角度看C++11初始化对象的语法选择既丰富得让人尴尬又混乱得让人糊涂。一般来说初始化值要用()或者{}括起来或者放到等号"="的右边:
````cpp
int x(0); //使用小括号初始化
int x(0); //使用小括号初始化
int y = 0; //使用"="初始化
int y = 0; //使用"="初始化
int z{ 0 }; //使用花括号初始化
int z{ 0 }; //使用花括号初始化
````
在很多情况下,你可以使用"="和花括号的组合:
````cpp
int z = { 0 }; //使用"="和花括号
int z = { 0 }; //使用"="和花括号
````
在这个条款的剩下部分,我通常会忽略"="和花括号组合初始化的语法因为C++通常把它视作和只有花括号一样。
@ -39,24 +39,24 @@ C++11使用统一初始化*uniform initialization*)来整合这些混乱且
括号初始化让你可以表达以前表达不出的东西。使用花括号,指定一个容器的元素变得很容易:
````cpp
std::vector<int> v{ 1, 3, 5 }; //v初始内容为1,3,5
std::vector<int> v{ 1, 3, 5 }; //v初始内容为1,3,5
````
括号初始化也能被用于为非静态数据成员指定默认初始值。C++11允许"="初始化不加花括号也拥有这种能力:
````cpp
class Widget{
...
private:
int x{ 0 }; //没问题x初始值为0
int y = 0; //也可以
int z(0); //错误!
int x{ 0 }; //没问题x初始值为0
int y = 0; //也可以
int z(0); //错误!
}
````
另一方面,不可拷贝的对象(例如`std::atomic`——见[Item40](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/7.TheConcurrencyAPI/item40.md))可以使用花括号初始化或者小括号初始化,但是不能使用"="初始化:
````cpp
std::atomic<int> ai1{ 0 }; //没问题
std::atomic<int> ai2(0); //没问题
std::atomic<int> ai3 = 0; //错误!
std::atomic<int> ai1{ 0 }; //没问题
std::atomic<int> ai2(0); //没问题
std::atomic<int> ai3 = 0; //错误!
````
因此我们很容易理解为什么括号初始化又叫统一初始化在C++中这三种方式都被指派为初始化表达式,但是只有括号任何地方都能被使用。
@ -64,26 +64,26 @@ std::atomic<int> ai3 = 0; //错误!
````cpp
double x, y, z;
int sum1{ x + y + z }; //错误double的和可能不能表示为int
int sum1{ x + y + z }; //错误double的和可能不能表示为int
````
使用小括号和"="的初始化不检查是否转换为变窄转换,因为由于历史遗留问题它们必须要兼容老旧代码:
````cpp
int sum2(x + y +z); //可以表达式的值被截为int
int sum2(x + y +z); //可以表达式的值被截为int
int sum3 = x + y + z; //同上
int sum3 = x + y + z; //同上
````
另一个值得注意的特性是括号表达式对于C++最令人头疼的解析问题有天生的免疫性。(译注:所谓最令人头疼的解析即*most vexing parse*,更多信息请参见[https://en.wikipedia.org/wiki/Most_vexing_parse](https://en.wikipedia.org/wiki/Most_vexing_parse)。C++规定任何能被决议为一个声明的东西必须被决议为声明。这个规则的副作用是让很多程序员备受折磨:当他们想创建一个使用默认构造函数构造的对象,却不小心变成了函数声明。问题的根源是如果你想使用一个实参调用一个构造函数,你可以这样做:
````cpp
Widget w1(10); //使用实参10调用Widget的一个构造函数
Widget w1(10); //使用实参10调用Widget的一个构造函数
````
但是如果你尝试使用相似的语法调用没有参数的`Widget`构造函数,它就会变成函数声明:
````cpp
Widget w2(); //最令人头疼的解析声明一个函数w2返回Widget
Widget w2(); //最令人头疼的解析声明一个函数w2返回Widget
````
由于函数声明中形参列表不能使用花括号,所以使用花括号初始化表明你想调用默认构造函数构造对象就没有问题:
````cpp
Widget w3{}; //调用没有参数的构造函数构造对象
Widget w3{}; //调用没有参数的构造函数构造对象
````
关于括号初始化还有很多要说的。它的语法能用于各种不同的上下文它防止了隐式的变窄转换而且对于C++最令人头疼的解析也天生免疫。既然好到这个程度那为什么这个条款不叫“Prefer braced initialization syntax”呢
@ -93,23 +93,23 @@ Widget w3{}; //调用没有参数的构造函数构造对象
````cpp
class Widget {
public:
Widget(int i, bool b); //构造函数未声明
Widget(int i, double d); //std::initializer_list形参
...
Widget(int i, bool b); //构造函数未声明
Widget(int i, double d); //std::initializer_list形参
};
Widget w1(10, true); //调用第一个构造函数
Widget w2{10, true}; //也调用第一个构造函数
Widget w3(10, 5.0); //调用第二个构造函数
Widget w4{10, 5.0}; //也调用第二个构造函数
Widget w1(10, true); //调用第一个构造函数
Widget w2{10, true}; //也调用第一个构造函数
Widget w3(10, 5.0); //调用第二个构造函数
Widget w4{10, 5.0}; //也调用第二个构造函数
````
然而,如果有一个或者多个构造函数的声明一个`std::initializer_list`形参,使用括号初始化语法的调用更倾向于适用`std::initializer_list`重载函数。而且只要某个使用括号表达式的调用能适用接受`std::initializer_list`的构造函数,编译器就会使用它。如果上面的`Widget`类有一个`std::initializer_list<long double>`构造函数并被传入实参,就像这样:
````cpp
class Widget {
public:
Widget(int i, bool b); //同上
Widget(int i, double d); //同上
Widget(std::initializer_list<long double> il); //新添加的
...
Widget(int i, bool b); //同上
Widget(int i, double d); //同上
Widget(std::initializer_list<long double> il); //新添加的
};
````
`w2`和`w4`将会使用新添加的构造函数构造,即使另一个非`std::initializer_list`构造函数对于实参是更好的选择:
@ -133,34 +133,34 @@ Widget w4{10, 5.0}; //使用花括号初始化,但是现在
````cpp
class Widget {
public:
Widget(int i, bool b); //同之前一样
Widget(int i, double d); //同之前一样
Widget(std::initializer_list<long double> il); //同之前一样
operator float() const; //转换为float
...
Widget(int i, bool b); //同之前一样
Widget(int i, double d); //同之前一样
Widget(std::initializer_list<long double> il); //同之前一样
operator float() const; //转换为float
};
Widget w5(w4); //使用小括号,调用拷贝构造函数
Widget w5(w4); //使用小括号,调用拷贝构造函数
Widget w6{w4}; //使用花括号调用std::initializer_list构造
//函数w4转换为floatfloat转换为double
Widget w6{w4}; //使用花括号调用std::initializer_list构造
//函数w4转换为floatfloat转换为double
Widget w7(std::move(w4)); //使用小括号,调用移动构造函数
Widget w7(std::move(w4)); //使用小括号,调用移动构造函数
Widget w8{std::move(w4)}; //使用花括号调用std::initializer_list构造
//函数与w6相同原因
Widget w8{std::move(w4)}; //使用花括号调用std::initializer_list构造
//函数与w6相同原因
````
编译器热衷于把括号初始化与使`std::initializer_list`构造函数匹配了,尽管最佳匹配`std::initializer_list`构造函数不能被调用也会凑上去。比如:
````cpp
class Widget {
public:
Widget(int i, bool b); //同之前一样
Widget(int i, double d); //同之前一样
Widget(std::initializer_list<bool> il); //现在元素类型为bool
... //没有隐式转换函数
Widget(int i, bool b); //同之前一样
Widget(int i, double d); //同之前一样
Widget(std::initializer_list<bool> il); //现在元素类型为bool
//没有隐式转换函数
};
Widget w{10, 5.0}; //错误!要求变窄转换
Widget w{10, 5.0}; //错误!要求变窄转换
````
这里,编译器会直接忽略前面两个构造函数(其中第二个提供了所有实参类型的最佳匹配),然后尝试调用`std::initializer_list<bool>`构造函数。调用这个函数将会把`int(10)`和`double(5.0)`转换为`bool`,由于会产生变窄转换(`bool`不能准确表示其中任何一个值),括号初始化拒绝变窄转换,所以这个调用无效,代码无法通过编译。
@ -169,11 +169,11 @@ Widget w{10, 5.0}; //错误!要求变窄转换
````cpp
class Widget {
public:
Widget(int i, bool b); //同之前一样
Widget(int i, double d); //同之前一样
//现在std::initializer_list元素类型为std::string
Widget(std::initializer_list<std::string> il);
... //没有隐式转换函数
Widget(int i, bool b); //同之前一样
Widget(int i, double d); //同之前一样
//现在std::initializer_list元素类型为std::string
Widget(std::initializer_list<std::string> il);
//没有隐式转换函数
};
Widget w1(10, true); // 使用小括号初始化,调用第一个构造函数
@ -187,20 +187,20 @@ Widget w4{10, 5.0}; // 使用花括号初始化,现在调用第二个构
````cpp
class Widget {
public:
Widget(); //默认构造函数
Widget(std::initializer_list<int> il); //std::initializer_list构造函数
Widget(); //默认构造函数
Widget(std::initializer_list<int> il); //std::initializer_list构造函数
... //没有隐式转换函数
//没有隐式转换函数
};
Widget w1; //调用默认构造函数
Widget w2{}; //也调用默认构造函数
Widget w3(); //最令人头疼的解析!声明一个函数
Widget w1; //调用默认构造函数
Widget w2{}; //也调用默认构造函数
Widget w3(); //最令人头疼的解析!声明一个函数
````
如果你**想**用空`std::initializer`来调用`std::initializer_list`构造函数,你就得创建一个空花括号作为函数实参——通过把空花括号放在小括号或者另一花括号内来界定你想传递的东西。
````cpp
Widget w4({}); //使用空花括号列表调用std::initializer_list构造函数
Widget w5{{}}; //同上
Widget w4({}); //使用空花括号列表调用std::initializer_list构造函数
Widget w5{{}}; //同上
````
此时,括号初始化,`std::initializer_list`和构造函数重载的晦涩规则就会一下子涌进你的脑袋,你可能会想研究了半天这些东西在你的日常编程中到底占多大比例。可能比你想象的要多。因为`std::vector`作为其中之一会直接受到影响。`std::vector`有一个非`std::initializer_list`构造函数允许你去指定容器的初始大小,以及使用一个值填满你的容器。但它也有一个`std::initializer_list`构造函数允许你使用花括号里面的值初始化容器。如果你创建一个数值类型的`std::vector`(比如`std::vector<int>`),然后你传递两个实参,把这两个实参放到小括号和放到花括号中大不相同:
@ -225,19 +225,19 @@ template<typename T, //要创建的对象类型
typename... Ts> //要使用的实参的类型
void doSomeWork(Ts&&... params)
{
create local T object from params...
...
create local T object from params...
}
````
在现实中我们有两种方式实现这个伪代码(关于`std::forward`请参见[Item25](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item25.md)
````cpp
T localObject(std::forward<Ts>(params)...); //使用小括号
T localObject{std::forward<Ts>(params)...}; //使用花括号
T localObject(std::forward<Ts>(params)...); //使用小括号
T localObject{std::forward<Ts>(params)...}; //使用花括号
````
考虑这样的调用代码:
````cpp
std::vector<int> v;
...
doSomeWork<std::vector<int>>(10, 20);
````
如果`doSomeWork`创建`localObject`时使用的是小括号,`std::vector`就会包含10个元素。如果`doSomeWork`创建`localObject`时使用的是花括号,`std::vector`就会包含2个元素。哪个是正确的`doSomeWork`的作者不知道,只有调用者知道。