mirror of
https://github.com/CnTransGroup/EffectiveModernCppChinese.git
synced 2025-02-01 14:50:29 +08:00
commit
17b7aef5f0
@ -187,4 +187,4 @@ std::shared_ptr<Investment> sp = makeInvestment(arguments);
|
|||||||
|
|
||||||
- `std::unique_ptr`是轻量级、快速的、只能move的管理专有所有权语义资源的智能指针
|
- `std::unique_ptr`是轻量级、快速的、只能move的管理专有所有权语义资源的智能指针
|
||||||
- 默认情况,资源销毁通过delete,但是支持自定义delete函数。有状态的删除器和函数指针会增加`std::unique_ptr`的大小
|
- 默认情况,资源销毁通过delete,但是支持自定义delete函数。有状态的删除器和函数指针会增加`std::unique_ptr`的大小
|
||||||
- 将`std::unique_ptr`转化为`std::shared+ptr`是简单的
|
- 将`std::unique_ptr`转化为`std::shared_ptr`是简单的
|
@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
让我们从已知很多类型不支持移动操作开始这个过程。为了升级到C++11,C++98的很多标准库做了大修改,为很多类型提供了移动的能力,这些类型的移动实现比复制操作更快,并且对库的组件实现修改以利用移动操作。但是很有可能你工作中的代码没有完整地利用C++11。对于你的应用中(或者代码库中),没有适配C++11的部分,编译器即使支持移动语义也是无能为力的。的确,C++11倾向于为缺少移动操作定义的类默认生成,但是只有在没有声明复制操作,移动操作,或析构函数的类中才会生成移动操作(参考Item17)。禁止移动操作的类中(通过delete move operation 参考Item11),编译器不生成移动操作的支持。对于没有明确支持移动操作的类型,并且不符合编译器默认生成的条件的类,没有理由期望C++11会比C++98进行任何性能上的提升。
|
让我们从已知很多类型不支持移动操作开始这个过程。为了升级到C++11,C++98的很多标准库做了大修改,为很多类型提供了移动的能力,这些类型的移动实现比复制操作更快,并且对库的组件实现修改以利用移动操作。但是很有可能你工作中的代码没有完整地利用C++11。对于你的应用中(或者代码库中),没有适配C++11的部分,编译器即使支持移动语义也是无能为力的。的确,C++11倾向于为缺少移动操作定义的类默认生成,但是只有在没有声明复制操作,移动操作,或析构函数的类中才会生成移动操作(参考Item17)。禁止移动操作的类中(通过delete move operation 参考Item11),编译器不生成移动操作的支持。对于没有明确支持移动操作的类型,并且不符合编译器默认生成的条件的类,没有理由期望C++11会比C++98进行任何性能上的提升。
|
||||||
|
|
||||||
即使显式支持了移动操作,结果可能也没有你希望的那么好。比如,所有C++11的标准库都支持了移动操作,但是任务移动所有容器的开销都非常小是个错误。对于某些容器来说,压根就不存在开销小的方式来移动它所包含的内容。对另一些容器来说,开销真正小的移动操作却使得容器元素移动含义事与愿违。
|
即使显式支持了移动操作,结果可能也没有你希望的那么好。比如,所有C++11的标准库都支持了移动操作,但是认为移动所有容器的开销都非常小是个错误。对于某些容器来说,压根就不存在开销小的方式来移动它所包含的内容。对另一些容器来说,开销真正小的移动操作却使得容器元素移动含义事与愿违。
|
||||||
|
|
||||||
考虑一下`std::array`,这是C++11中的新容器。`std::array`本质上是具有STL接口的内置数组。这与其他标准容器将内容存储在堆内存不同。存储具体数据在堆内存的容器,本身只保存了只想堆内存数据的指针(真正实现当然更复杂一些,但是基本逻辑就是这样)。这种实现使得在常数时间移动整个容器成为可能的,只需要拷贝容器中保存的指针到目标容器,然后将原容器的指针置为空指针就可以了。
|
考虑一下`std::array`,这是C++11中的新容器。`std::array`本质上是具有STL接口的内置数组。这与其他标准容器将内容存储在堆内存不同。存储具体数据在堆内存的容器,本身只保存了只想堆内存数据的指针(真正实现当然更复杂一些,但是基本逻辑就是这样)。这种实现使得在常数时间移动整个容器成为可能的,只需要拷贝容器中保存的指针到目标容器,然后将原容器的指针置为空指针就可以了。
|
||||||
|
|
||||||
@ -24,7 +24,7 @@ std::array<Widget, 10000> aw1;
|
|||||||
auto aw2 = std::move(aw1); // move aw1 into aw2. Runs in linear time. All elements in aw1 are moved into aw2.
|
auto aw2 = std::move(aw1); // move aw1 into aw2. Runs in linear time. All elements in aw1 are moved into aw2.
|
||||||
```
|
```
|
||||||
|
|
||||||
注意`aw1`中的元素被移动到了`aw2`中,这里假定`Widget`类的移动操作比复制操作快。但是使用`std::array`的移动操作还是复制操作都将话费线性时间的开销,因为每个容器中的元素终归需要拷贝一次,这与“移动一个容器就像操作几个指针一样方便”的含义想去甚远。
|
注意`aw1`中的元素被移动到了`aw2`中,这里假定`Widget`类的移动操作比复制操作快。但是使用`std::array`的移动操作还是复制操作都将花费线性时间的开销,因为每个容器中的元素终归需要拷贝一次,这与“移动一个容器就像操作几个指针一样方便”的含义想去甚远。
|
||||||
|
|
||||||
另一方面,`std::strnig`提供了常数时间的移动操作和线性时间的复制操作。这听起来移动比复制快多了,但是可能不一定。许多字符串的实现采用了*small string optimization(SSO)*。"small"字符串(比如长度小于15个字符的)存储在了`std::string`的缓冲区中,并没有存储在堆内存,移动这种存储的字符串并不必复制操作更快。
|
另一方面,`std::strnig`提供了常数时间的移动操作和线性时间的复制操作。这听起来移动比复制快多了,但是可能不一定。许多字符串的实现采用了*small string optimization(SSO)*。"small"字符串(比如长度小于15个字符的)存储在了`std::string`的缓冲区中,并没有存储在堆内存,移动这种存储的字符串并不必复制操作更快。
|
||||||
|
|
||||||
|
@ -0,0 +1,214 @@
|
|||||||
|
## Item25: 对右值引用使用`std::move`,对通用引用使用`std::forward`
|
||||||
|
|
||||||
|
右值引用仅绑定可以移动的对象。如果你有一个右值引用参数,你就知道这个对象可能会被移动:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
class Widget {
|
||||||
|
Widget(Widget&& rhs); //rhs definitely refers to an object eligible for moving
|
||||||
|
...
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
这是个例子,你将希望通过可以利用该对象右值性的方式传递给其他使用对象的函数。这样做的方法是将绑定次类对象的参数转换为右值。如Item23中所述,这不仅是`std::move`所做,而且是为它创建:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
class Widget {
|
||||||
|
public:
|
||||||
|
Widget(Widget&& rhs) :name(std::move(rhs.name)), p(std::move(rhs.p)) {...}
|
||||||
|
...
|
||||||
|
private:
|
||||||
|
std::string name;
|
||||||
|
std::shared_ptr<SomeDataStructure> p;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
另一方面(查看Item24),通用引用可能绑定到有资格移动的对象上。通用引用使用右值初始化时,才将其强制转换为右值。Item23阐释了这正是`std::forward`所做的:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
class Widget {
|
||||||
|
public:
|
||||||
|
template<typename T>
|
||||||
|
void setName(T&& newName) { //newName is universal reference
|
||||||
|
name = std::forward<T>(newName);
|
||||||
|
}
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
总而言之,当传递给函数时右值引用应该无条件转换为右值(通过`std::move`),通用引用应该有条件转换为右值(通过`std::forward`)。
|
||||||
|
|
||||||
|
Item23 解释说,可以在右值引用上使用`std::forward`表现出适当的行为,但是代码较长,容易出错,所以应该避免在右值引用上使用`std::forward`。更糟的是在通用引用上使用`std::move`,这可能会意外改变左值。
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
class Widget {
|
||||||
|
public:
|
||||||
|
template<typename T>
|
||||||
|
void setName(T&& newName) {
|
||||||
|
name = std::move(newName); //universal reference compiles, but is bad ! bad ! bad !
|
||||||
|
}
|
||||||
|
...
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::string name;
|
||||||
|
std::shared_ptr<SomeDataStructure> p;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::string getWidgetName(); // factory function
|
||||||
|
|
||||||
|
Widget w;
|
||||||
|
auto n = getWidgetName(); // n is local variiable
|
||||||
|
w.setName(n); // move n into w! n's value now unkown
|
||||||
|
```
|
||||||
|
|
||||||
|
上面的例子,局部变量n被传递给`w.setName`,可以调用方对n只有只读操作。但是因为`setName`内部使用`std::move`无条件将传递的参数转换为右值,`n`的值被移动给w,n最终变为未定义的值。这种行为使得调用者蒙圈了。
|
||||||
|
|
||||||
|
你可能争辩说`setName`不应该将其参数声明为通用引用。此类引用不能使用`const`(Item 24),但是`setName`肯定不应该修改其参数。你可能会指出,如果const左值和右值分别进行重载可以避免整个问题,比如这样:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
class Widget {
|
||||||
|
public:
|
||||||
|
void setName(const std::string& newName) { // set from const lvalue
|
||||||
|
name = newName;
|
||||||
|
}
|
||||||
|
void setName(std::string&& newName) { // set from rvalue
|
||||||
|
name = std::move(newName);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
这样的话,当然可以工作,但是有缺点。首先编写和维护的代码更多;其次,效率下降。比如,考虑如下场景:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
w.setName("Adela Novak");
|
||||||
|
```
|
||||||
|
|
||||||
|
使用通用引用的版本,字面字符串"Adela Novak"可以被传递给setName,在w内部使用了`std::string`的赋值运算符。w的name的数据成员直接通过字面字符串直接赋值,没有中间对象被创建。但是,重载版本,会有一个中间对象被创建。一次setName的调用会包括`std::string`的构造器调用(中间对象),`std::string`的赋值运算调用,`std::string`的析构调用(中间对象)。这比直接通过const char*赋值给`std::string`开销昂贵许多。实际的开销可能因为库的实现而有所不同,但是事实上,将通用引用模板替换成多个函数重载在某些情况下会导致运行时的开销。如果例子中的`Widget`数据成员是任意类型(不一定是`std::string`),性能差距可能会变得更大,因为不是所有类型的移动操作都像`std::string`开销较小(参看Item29)。
|
||||||
|
|
||||||
|
但是,关于重载函数最重要的问题不是源代码的数量,也不是代码的运行时性能。而是设计的可扩展性差。`Widget::setName`接受一个参数,可以是左值或者右值,因此需要两种重载实现,`n`个参数的话,就要实现$2^n$种重载。这还不是最坏的。有的函数---函数模板----接受无限制参数,每个参数都可以是左值或者右值。此类函数的例子比如`std::make_unique`或者`std::make_shared`。查看他们的的重载声明:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
template<class T, class... Args>
|
||||||
|
shared_ptr<T> make_shared(Args&&... args);
|
||||||
|
|
||||||
|
template<class T, class... Args>
|
||||||
|
unique_ptr<T> make_unique(Args&&... args);
|
||||||
|
```
|
||||||
|
|
||||||
|
对于这种函数,对于左值和右值分别重载就不能考虑了:通用引用是仅有的实现方案。对这种函数,我向你保证,肯定使用`std::forward`传递通用引用给其他函数。
|
||||||
|
|
||||||
|
好吧,通常,最终。但是不一定最开始就是如此。在某些情况,你可能需要在一个函数中多次使用绑定到右值引用或者通用引用的对象,并且确保在完成其他操作前,这个对象不会被移动。这时,你只想在最后一次使用时,使用`std::move`或者`std::forward`。比如:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
template<typename T>
|
||||||
|
void setSignText(T&& text)
|
||||||
|
{
|
||||||
|
sign.setText(text);
|
||||||
|
|
||||||
|
auto now = std::chrono::system_clock::now();
|
||||||
|
|
||||||
|
signHistory.add(now, std::forward<T>(text));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
这里,我们想要确保`text`的值不会被`sign.setText`改变,因为我们想要在`signHistory.add`中继续使用。因此`std::forward`只在最后使用。
|
||||||
|
|
||||||
|
对于`std::move`,同样的思路,但是需要注意,在有些稀少的情况下,你需要调用`std::move_if_noexcept`代替`std::move`。要了解何时以及为什么,参考Item 14。
|
||||||
|
|
||||||
|
如果你使用的按值返回的函数,并且返回值绑定到右值引用或者通用引用上,需要对返回的引用使用`std::move`或者`std::forward`。要了解原因,考虑`+`操作两个矩阵的函数,左侧的矩阵参数为右值(可以被用来保存求值之后的和)
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
Matrix operator+(Matrix&& lhs, const Matrix& rhs){
|
||||||
|
lhs += rhs;
|
||||||
|
return std::move(lhs); // move lhs into return value
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
通过在返回语句中将lhs转换为右值,lhs可以移动到返回值的内存位置。如果`std::move`省略了
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
Matrix operator+(Matrix&& lhs, const Matrix& rhs){
|
||||||
|
lhs += rhs;
|
||||||
|
return lhs; // copy lhs into return value
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
事实上,lhs作为左值,会被编译器拷贝到返回值的内存空间。假定Matrix支持移动操作,并且比拷贝操作效率更高,使用`std::move`的代码效率更高。
|
||||||
|
|
||||||
|
如果Matrix不支持移动操作,将其转换为左值不会变差,因为右值可以直接被Matrix的拷贝构造器使用。如果Matrix随后支持了移动操作,`+`操作符的定义将在下一次编译时受益。就是这种情况,通过将`std::move`应用到返回语句中,不会损失什么,还可能获得收益。
|
||||||
|
|
||||||
|
使用通用引用和`std::forward`的情况类似。考虑函数模板`reduceAndCopy`收到一个未规约对象`Fraction`,将其规约,并返回一个副本。如果原始对象是右值,可以将其移动到返回值中,避免拷贝开销,但是如果原始对象是左值,必须创建副本,因此如下代码:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
template<typename T>
|
||||||
|
Fraction reduceAndCopy(T&& frac) {
|
||||||
|
frac.reduce();
|
||||||
|
return std::forward<T>(frac); // move rvalue into return value, copy lvalue
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
如果`std::forward`被忽略,frac就是无条件复制到返回值内存空间。
|
||||||
|
|
||||||
|
有些开发者获取到上面的知识后,并尝试将其扩展到不适用的情况。
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
Widget makeWidget() {
|
||||||
|
Widget w; //local variable
|
||||||
|
... // configure w
|
||||||
|
return w; // "copy" w into return value
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
想要优化copy的动作为如下代码:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
Widget makeWidget() {
|
||||||
|
Widget w; //local variable
|
||||||
|
... // configure w
|
||||||
|
return std::move(w); // move w into return value(don't do this!)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
这种用法是有问题的,但是问题在哪?
|
||||||
|
|
||||||
|
在进行优化时,标准化委员会远领先于开发者,第一个版本的makeWidget可以在分配给函数返回值的内存中构造局部变量w来避免复制局部变量w的需要。这就是所谓的返回值优化(RVO),这在C++标准中已经实现了。
|
||||||
|
|
||||||
|
所以"copy"版本的makeWidget在编译时都避免了拷贝局部变量w,进行了返回值优化。(返回值优化的条件:1. 局部变量与返回值的类型相同;2. 局部变量就是返回值)。
|
||||||
|
|
||||||
|
移动版本的makeWidget行为与其名称一样,将w的内容移动到makeWidget的返回值位置。但是为什么编译器不使用RVO消除这种移动,而是在分配给函数返回值的内存中再次构造w呢?条件2中规定,仅当返回值为局部对象时,才进行RVO,但是move版本不满足这条件,再次看一下返回语句:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
return std::move(w);
|
||||||
|
```
|
||||||
|
|
||||||
|
返回的已经不是局部对象w,而是局部对象w的引用。返回局部对象的引用不满足RVO的第二个条件,所以编译器必须移动w到函数返回值的位置。开发者试图帮助编译器优化反而限制了编译器的优化选项。
|
||||||
|
|
||||||
|
(译者注:本段即绕又长,大意为即使开发者非常熟悉编译器,坚持要在局部变量上使用`std::move`返回)
|
||||||
|
|
||||||
|
这仍然是一个坏主意。C++标准关于RVO的部分表明,如果满足RVO的条件,但是编译器选择不执行复制忽略,则必须将返回的对象视为右值。实际上,标准要求RVO,忽略复制或者将`sdt::move`隐式应用于返回的本地对象。因此,在makeWidget的"copy"版本中,编译器要不执行复制忽略的优化,要不自动将`std::move`隐式执行。
|
||||||
|
|
||||||
|
按值传递参数的情形与此类似。他们没有资格进行RVO,但是如果作为返回值的话编译器会将其视作右值。结果就是,如果代码如下:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
Widget makeWidget(Widget w) {
|
||||||
|
...
|
||||||
|
return w;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
实际上,编译器的代码如下:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
Widget makeWidget(Widget w){
|
||||||
|
...
|
||||||
|
return std::move(w);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
这意味着,如果对从按值返回局部对象的函数使用`std::move`,你并不能帮助编译器,而是阻碍其执行优化选项。在某些情况下,将`std::move`应用于局部变量可能是一件合理的事,但是不要阻碍编译器RVO。
|
||||||
|
|
||||||
|
### 需要记住的点
|
||||||
|
|
||||||
|
- 在右值引用上使用`std::move`,在通用引用上使用`std::forward`
|
||||||
|
- 对按值返回的函数返回值,无论返回右值引用还是通用引用,执行相同的操作
|
||||||
|
- 当局部变量就是返回值是,不要使用`std::move`或者`std::forward`
|
||||||
|
|
@ -0,0 +1,2 @@
|
|||||||
|
## Item26: 避免在通用引用上重载
|
||||||
|
|
@ -0,0 +1,2 @@
|
|||||||
|
## Item27:熟悉通用引用重载的替代方法
|
||||||
|
|
@ -0,0 +1,205 @@
|
|||||||
|
## Item28:理解引用折叠
|
||||||
|
|
||||||
|
Item23中指出,当参数传递给模板函数时,模板参数的类型是左值还是右值被推导出来。但是并没有提到只有当参数被声明为通用引用时,上述推导才会发生,但是有充分的理由忽略这一点:因为通用引用是Item24中才提到。回过头来看,通用引用和左值/右值编码意味着:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
template<typename T>
|
||||||
|
void func(T&& param);
|
||||||
|
```
|
||||||
|
|
||||||
|
被推导的模板参数T将根据被传入参数类型被编码为左值或者右值。
|
||||||
|
|
||||||
|
编码机制是简单的。当左值被传入时,T被推导为左值。当右值被传入时,T被推导为非引用(请注意不对称性:左值被编码为左值引用,右值被编码为非引用),因此:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
Widget widgetFactory(); // function returning rvalue
|
||||||
|
Widget w; // a variable(an lvalue)
|
||||||
|
func(w); // call func with lvalue; T deduced to be Widget&
|
||||||
|
func(widgetFactory()); // call func with rvalue; T deduced to be Widget
|
||||||
|
```
|
||||||
|
|
||||||
|
上面的两种调用中,Widget被传入,因为一个是左值,一个是右值,模板参数T被推导为不同的类型。正如我们很快看到的,这决定了通用引用成为左值还是右值,也是`std::forward`的工作基础。
|
||||||
|
|
||||||
|
在我们更加深入`std::forward`和通用引用之前,必须明确在C++中引用的引用是非法的。不知道你是否尝试过下面的写法,编译器会报错:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
int x;
|
||||||
|
...
|
||||||
|
auto& & rx = x; //error! can't declare reference to reference
|
||||||
|
```
|
||||||
|
|
||||||
|
考虑下,如果一个左值传给模板函数的通用引用会发生什么:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
template<typename T>
|
||||||
|
void func(T&& param);
|
||||||
|
|
||||||
|
func(w); // invoke func with lvalue; T deduced as Widget&
|
||||||
|
```
|
||||||
|
|
||||||
|
如果我们把推导出来的类型带入回代码中看起来就像是这样:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
void func(Widget& && param);
|
||||||
|
```
|
||||||
|
|
||||||
|
引用的引用!但是编译器没有报错。我们从Item24中了解到因为通用引用param被传入一个左值,所以param的类型被推导为左值引用,但是编译器如何采用T的推导类型的结果,这是最终的函数签名?
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
void func(Widget& param);
|
||||||
|
```
|
||||||
|
|
||||||
|
答案是引用折叠。是的,禁止你声明引用的引用,但是编译器会在特定的上下文中使用,包括模板实例的例子。当编译器生成引用的引用时,引用折叠指导下一步发生什么。
|
||||||
|
|
||||||
|
存在两种类型的引用(左值和右值),所以有四种可能的引用组合(左值的左值,左值的右值,右值的右值,右值的左值)。如果一个上下文中允许引用的引用存在(比如,模板函数的实例化),引用根据规则折叠为单个引用:
|
||||||
|
|
||||||
|
> 如果任一引用为左值引用,则结果为左值引用。否则(即,如果引用都是右值引用),结果为右值引用
|
||||||
|
|
||||||
|
在我们上面的例子中,将推导类型Widget&替换模板func会产生对左值引用的右值引用,然后引用折叠规则告诉我们结果就是左值引用。
|
||||||
|
|
||||||
|
引用折叠是`std::forward`工作的一种关键机制。就像Item25中解释的一样,`std::forward`应用在通用引用参数上,所以经常能看到这样使用:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
template<typename T>
|
||||||
|
void f(T&& fParam)
|
||||||
|
{
|
||||||
|
... // do some work
|
||||||
|
someFunc(std::forward<T>(fParam)); // forward fParam to someFunc
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
因为fParam是通用引用,我们知道参数T的类型将在传入具体参数时被编码。`std::forward`的作用是当传入参数为右值时,即T为非引用类型,才将fParam(左值)转化为一个右值。
|
||||||
|
|
||||||
|
`std::forward`可以这样实现:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
template<typename T>
|
||||||
|
T&& forward(typename remove_reference<T>::type& param)
|
||||||
|
{
|
||||||
|
return static_cast<T&&>(param);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
这不是标准库版本的实现(忽略了一些接口描述),但是为了理解`std::forward`的行为,这些差异无关紧要。
|
||||||
|
|
||||||
|
假设传入到f的Widget的左值类型。T被推导为Widget&,然后调用`std::forward`将初始化为`std::forward<Widget&>`。带入到上面的`std::forward`的实现中:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
Widget& && forward(typename remove_reference<Widget&>::type& param)
|
||||||
|
{
|
||||||
|
return static_cast<Widget& &&>(param);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`std::remove_reference<Widget&>::type`表示Widget(查看Item9),所以`std::forward`成为:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
Widget& && forward(Widget& param)
|
||||||
|
{
|
||||||
|
return static_cast<Widget& &&>(param);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
根据引用折叠规则,返回值和static_cast可以化简,最终版本的`std::forward`就是
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
Widget& forward(Widget& param)
|
||||||
|
{
|
||||||
|
return static_cast<Widget&>(param);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
正如你所看到的,当左值被传入到函数模板f时,`std::forward`转发和返回的都是左值引用。内部的转换不做任何事,因为param的类型已经是`Widget&`,所以转换没有影响。左值传入会返回左值引用。通过定义,左值引用就是左值,因此将左值传递给`std::forward`会返回左值,就像说的那样,完美转发。
|
||||||
|
|
||||||
|
现在假设一下,传递给f的是一个`Widget`的右值。在这个例子中,T的类型推导就是Widget。内部的`std::forward`因此转发`std::forward<Widget>`,带入回`std::forward`实现中:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
Widget&& forward(typename remove_reference<Widget>::type& param)
|
||||||
|
{
|
||||||
|
return static_cast<Widget&&>(param);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
将`remove_reference`引用到非引用的类型上还是相同的类型,所以化简如下
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
Widget&& forward(Widget& param)
|
||||||
|
{
|
||||||
|
return static_cast<Widget&&>(param);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
这里没有引用的引用,所以不需要引用折叠,这就是最终版本。
|
||||||
|
|
||||||
|
从函数返回的右值引用被定义为右值,因此在这种情况下,`std::forward`会将f的参数fParam(左值)转换为右值。最终结果是,传递给f的右值参数将作为右值转发给someFunc,完美转发。
|
||||||
|
|
||||||
|
在C++14中,`std::remove_reference_t`的存在使得实现变得更简单:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
template<typename T> // C++ 14; still in namepsace std
|
||||||
|
T&& forward(remove_reference_t<T>& param)
|
||||||
|
{
|
||||||
|
return static_cast<T&&>(param);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
引用折叠发生在四种情况下。**第一**,也是最常见的就是模板实例化。**第二**,是auto变量的类型生成,具体细节类似模板实例化的分析,因为类型推导基本与模板实例化雷同(参见Item2)。考虑下面的例子:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
template<typename T>
|
||||||
|
void func(T&& param);
|
||||||
|
Widget widgetFactory(); // function returning rvalue
|
||||||
|
Widget w; // a variable(an lvalue)
|
||||||
|
func(w); // call func with lvalue; T deduced to be Widget&
|
||||||
|
func(widgetFactory()); // call func with rvalue; T deduced to be Widget
|
||||||
|
```
|
||||||
|
|
||||||
|
在auto的写法中,规则是类似的:`auto&& w1 = w;`初始化`w1`为一个左值,因此为auto推导出类型`Widget&`。带回去就是`Widget& && w1 = w`,应用引用折叠规则,就是`Widget& w1 = w`,结果就是`w1`是一个左值引用。
|
||||||
|
|
||||||
|
另一方面,`auto&& w2 = widgetFactory();`使用右值初始化`w2`,非引用带回`Widget&& w2 = widgetFactory()`。没有引用的引用,这就是最终结果。
|
||||||
|
|
||||||
|
现在我们真正理解了Item24中引入的通用引用。通用引用不是一种新的引用,它实际上是满足两个条件下的右值引用:
|
||||||
|
|
||||||
|
- **通过类型推导将左值和右值区分**。T类型的左值被推导为&类型,T类型的右值被推导为T
|
||||||
|
- **引用折叠的发生**
|
||||||
|
|
||||||
|
通用引用的概念是有用的,因为它使你不必一定意识到引用折叠的存在,从直觉上判断左值和右值的推导即可。
|
||||||
|
|
||||||
|
我说了有四种情况会发生引用折叠,但是只讨论了两种:模板实例化和auto的类型生成。**第三**,是使用typedef和别名声明(参见Item9),如果,在创建或者定义typedef过程中出现了引用的引用,则引用折叠就会起作用。举例子来说,假设我们有一个Widget的类模板,该模板具有右值引用类型的嵌入式typedef:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
template<typename T>
|
||||||
|
class Widget {
|
||||||
|
public:
|
||||||
|
typedef T&& RvalueRefToT;
|
||||||
|
...
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
假设我们使用左值引用实例化Widget:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
Widget<int&> w;
|
||||||
|
```
|
||||||
|
|
||||||
|
就会出现
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
typedef int& && RvalueRefToT;
|
||||||
|
```
|
||||||
|
|
||||||
|
引用折叠就会发挥作用:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
typedef int& RvalueRefToT;
|
||||||
|
```
|
||||||
|
|
||||||
|
这清楚表明我们为typedef选择的name可能不是我们希望的那样:RvalueRefToT是左值引用的typedef,当使用Widget被左值引用实例化时。
|
||||||
|
|
||||||
|
最后,**也是第四**种情况是,decltype使用的情况,如果在分析decltype期间,出现了引用的引用,引用折叠规则就会起作用(关于decltype,参见Item3)
|
||||||
|
|
||||||
|
### 需要记住的事
|
||||||
|
|
||||||
|
- 引用折叠发生在四种情况:模板实例化;auto类型推导;typedef的创建和别名声明;decltype
|
||||||
|
- 当编译器生成了引用的引用时,结果通过引用折叠就是单个引用。有左值引用就是左值引用,否则就是右值引用
|
||||||
|
- 通用引用就是通过类型推导区分左值还是右值,并且引用折叠出现的右值引用
|
@ -0,0 +1,237 @@
|
|||||||
|
## Item30:熟悉完美转发的失败case
|
||||||
|
|
||||||
|
C++11最显眼的功能之一就是完美转发功能。完美转发,太棒了!哎,开始使用,你就发现“完美”,理想与现实还是有差距。C++11的完美转发是非常好用,但是只有当你愿意忽略一些失败情况,这个Item就是使你熟悉这些情形。
|
||||||
|
|
||||||
|
在我们开始epsilon探索之前,有必要回顾一下“完美转发”的含义。“转发”仅表示将一个函数的参数传递给另一个函数。对于被传递的第二个函数目标是收到与第一个函数完全相同的对象。这就排除了按值传递参数,因为它们是原始调用者传入内容的副本。我们希望被转发的函数能够可以与原始函数一起使用对象。指着参数也被排除在外,因为我们不想强迫调用者传入指针。关于通用转发,我们将处理引用参数。
|
||||||
|
|
||||||
|
完美转发意味着我们不仅转发对象,我们还转发显著的特征:它们的类型,是左值还是右值,是const还是volatile。结合到我们会处理引用参数,这意味着我们将使用通用引用(参见Item24),因为通用引用参数被传入参数时才确定是左值还是右值。
|
||||||
|
|
||||||
|
假定我们有一些函数f,然后想编写一个转发给它的函数(就使用一个函数模板)。我们需要的核心看起来像是这样:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
template<typename T>
|
||||||
|
void fwd(T&& param) // accept any argument
|
||||||
|
{
|
||||||
|
f(std::forward<T>(param)); // forward it to f
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
从本质上说,转发功能是通用的。例如fwd模板,接受任何类型的采纳数,并转发得到的任何参数。这种通用性的逻辑扩展是转发函数不仅是模板,而且是可变模板,因此可以接受任何数量的参数。fwd的可变个是如下:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
template<typename... Ts>
|
||||||
|
void fwd(Ts&&... params) // accept any arguments
|
||||||
|
{
|
||||||
|
f(std::forward<Ts>(params)...); // forward them to f
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
这种形式你会在标准化容器emplace中(参见Item42)和只能容器的工厂函数`std::make_unique和std::make_shared`中(参见Item21)看到。
|
||||||
|
|
||||||
|
给定我们的目标函数f和被转发的函数fwd,如果f使用特定参数做一件事,但是fwd使用相同的参数做另一件事,完美转发就会失败:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
f(expression); // if this does one thing
|
||||||
|
fwd(expression); // but this does something else, fwd fails to perfectly forward expression to f
|
||||||
|
```
|
||||||
|
|
||||||
|
导致这种失败的原因有很多。知道它们是什么以及如何解决它们很重要,因此让我们来看看那种参数无法做到完美转发。
|
||||||
|
|
||||||
|
### Braced initializers(支撑初始化器)
|
||||||
|
|
||||||
|
假定f这样声明:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
void f(const std::vector<int>& v);
|
||||||
|
```
|
||||||
|
|
||||||
|
在这个例子中,通过列表初始化器,
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
f({1,2,3}); // fine "{1,2,3}" implicitly converted to std::vector<int>
|
||||||
|
```
|
||||||
|
|
||||||
|
但是传递相同的列表初始化器给fwd不能编译
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
fwd({1,2,3}); // error! doesn't compile
|
||||||
|
```
|
||||||
|
|
||||||
|
这是因为这是完美转发失效的一种情况。
|
||||||
|
|
||||||
|
所有这种错误有相同的原因。在对f的直接调用(例如f({1,2,3})),编译器看到传入的参数是声明中的类型。如果类型不匹配,就会执行隐式转换操作使得调用成功。在上面的例子中,从`{1,2,3}`生成了临时变量`std::vector<int>`对象,因此f的参数会绑定到`std::vector<int>`对象上。
|
||||||
|
|
||||||
|
当通过调用函数模板fwd调用f时,编译器不再比较传入给fwd的参数和f的声明中参数的类型。代替的是,推导传入给fwd的参数类型,然后比较推导后的参数类型和f的声明类型。当下面情况任何一个发生时,完美转发就会失败:
|
||||||
|
|
||||||
|
- **编译器不能推导出一个或者多个fwd的参数类型**,编译器就会报错
|
||||||
|
- **编译器将一个或者多个fwd的参数类型推导错误**。在这里,“错误”可能意味着fwd将无法使用推导出的类型进行编译,但是也可能意味着调用者f使用fwd的推导类型对比直接传入参数类型表现出不一致的行为。这种不同行为的原因可能是因为f的函数重载定义,并且由于是“不正确的”类型推导,在fwd内部调用f和直接调用f将重载不同的函数。
|
||||||
|
|
||||||
|
在上面的`f({1,2,3})`例子中,问题在于,如标准所言,将括号初始化器传递给未声明为`std::initializer_list`的函数模板参数,该标准规定为“非推导上下文”。简单来讲,这意味着编译器在对fwd的调用中推导表达式`{1,2,3}`的类型,因为fwd的参数没有声明为`std::initializer_list`。对于fwd参数的推导类型被阻止,编译器只能拒绝该调用。
|
||||||
|
|
||||||
|
有趣的是,Item2 说明了使用**braced initializer**的auto的变量初始化的类型推导是成功的。这种变量被视为`std::initializer_list`对象,在转发函数应推导为`std::initializer_list`类型的情况,这提供了一种简单的解决方法----使用auto声明一个局部变量,然后将局部变量转发:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
auto il = {1,2,3}; // il's type deduced to be std::initializer_list<int>
|
||||||
|
fwd(il); // fine, perfect-forwards il to f
|
||||||
|
```
|
||||||
|
|
||||||
|
### 0或者NULL作为空指针
|
||||||
|
|
||||||
|
Item8说明当你试图传递0或者NULL作为空指针给模板时,类型推导会出错,推导为一个整数类型而不是指针类型。结果就是不管是0还是NULL都不能被完美转发为空指针。解决方法非常简单,使用nullptr就可以了,具体的细节,参考Item 8.
|
||||||
|
|
||||||
|
### 仅声明的整数静态const数据成员
|
||||||
|
|
||||||
|
通常,无需在类中定义整数静态const数据成员;声明就可以了。这是因为编译器会对此类成员
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
class Widget {
|
||||||
|
public:
|
||||||
|
static const std::size_t MinVals = 28; // MinVal's declaration
|
||||||
|
...
|
||||||
|
};
|
||||||
|
... // no defn. for MinVals
|
||||||
|
std::vector<int> widgetData;
|
||||||
|
widgetData.reserve(Widget::MinVals); // use of MinVals
|
||||||
|
```
|
||||||
|
|
||||||
|
这里,我们使用`Widget::MinVals`(或者简单点MinVals)来确定`widgetData`的初始容量,即使`MinVals`缺少定义。编译器通过将值28放入所有位置来补充缺少的定义。没有为`MinVals`的值留存储空间是没有问题的。如果要使用`MinVals`的地址(例如,有人创建了`MinVals`的指针),则`MinVals`需要存储(因为指针总要有一个地址),尽管上面的代码仍然可以编译,但是链接时就会报错,直到为`MinVals`提供定义。
|
||||||
|
|
||||||
|
按照这个思路,想象下f(转发参数给fwd的函数)这样声明:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
void f(std::size_t val);
|
||||||
|
```
|
||||||
|
|
||||||
|
使用`MinVals`调用f是可以的,因为编译器直接将值28代替`MinVals`:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
f(Widget::MinVals); // fine, treated as "28"
|
||||||
|
```
|
||||||
|
|
||||||
|
同样的,如果尝试通过fwd来调用f
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
fwd(Widget::MinVals); // error! shouldn't link
|
||||||
|
```
|
||||||
|
|
||||||
|
代码可以编译,但是不能链接。就像使用`MinVals`地址表现一样,确实,底层的问题是一样的。
|
||||||
|
|
||||||
|
尽管代码中没有使用`MinVals`的地址,但是fwd的参数是通用引用,而引用,在编译器生成的代码中,通常被视作指针。在程序的二进制底层代码中指针和引用是一样的。在这个水平下,引用只是可以自动取消引用的指针。在这种情况下,通过引用传递`MinVals`实际上与通过指针传递`MinVals`是一样的,因此,必须有内存使得指针可以指向。通过引用传递整型static const数据成员,必须定义它们,这个要求可能会造成完美转发失败,即使等效不使用完美转发的代码成功。(译者注:这里意思应该是没有定义,完美转发就会失败)
|
||||||
|
|
||||||
|
可能你也注意到了在上述讨论中我使用了一些模棱两可的词。代码“不应该”链接,引用“通常”被看做指针。传递整型static const数据成员“通常”要求定义。看起来就像有些事情我没有告诉你......
|
||||||
|
|
||||||
|
确实,根据标准,通过引用传递`MinVals`要求有定义。但不是所有的实现都强制要求这一点。所以,取决于你的编译器和链接器,你可能发现你可以在未定义的情况使用完美转发,恭喜你,但是这不是那样做的理由。为了具有可移植性,只要给整型static const提供一个定义,比如这样:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
const std::size_t Widget::MinVals; // in Widget's .cpp file
|
||||||
|
```
|
||||||
|
|
||||||
|
注意定义中不要重复初始化(这个例子中就是赋值28)。不要忽略这个细节,否则,编译器就会报错,提醒你只初始化一次。
|
||||||
|
|
||||||
|
### 重载的函数名称和模板名称
|
||||||
|
|
||||||
|
假定我们的函数f(通过fwd完美转发参数给f)可以通过向其传递执行某些功能的函数来定义其行为。假设这个函数参数和返回值都是整数,f声明就像这样:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
void f(int (*pf)(int)); // pf = "process function"
|
||||||
|
```
|
||||||
|
|
||||||
|
值得注意的是,也可以使用更简单的非指针语法声明。这种声明就像这样,含义与上面是一样的:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
void f(int pf(int)); // declares same f as above
|
||||||
|
```
|
||||||
|
|
||||||
|
无论哪种写法,我们都有了一个重载函数,processVal:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
int processVal(int value);
|
||||||
|
int processVal(int value, int priority);
|
||||||
|
```
|
||||||
|
|
||||||
|
我们可以传递processVal给f
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
f(processVal); // fine
|
||||||
|
```
|
||||||
|
|
||||||
|
但是有一点要注意,f要求一个函数指针,但是`processVal`不是一个函数指针或者一个函数,它是两个同名的函数。但是,编译器可以知道它需要哪个:通过参数类型和数量来匹配。因此选择了一个int参数的`processVal`地址传递给f
|
||||||
|
|
||||||
|
工作的基本机制是让编译器帮选择f的声明选择一个需要的`processVal`。但是,fwd是一个函数模板,没有需要的类型信息,使得编译器不可能帮助自动匹配一个合适的函数:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
fwd(processVal); // error! which processVal?
|
||||||
|
```
|
||||||
|
|
||||||
|
`processVal`没有类型信息,就不能类型推导,完美转发失败。
|
||||||
|
|
||||||
|
同样的问题会发生在如果我们试图使用函数模板代替重载的函数名。一个函数模板是未实例化的函数,表示一个函数族:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
template<typename T>
|
||||||
|
T workOnVal(T param) { ... } // template for processing values
|
||||||
|
fwd(workOnVal); // error! which workOnVal instantiation ?
|
||||||
|
```
|
||||||
|
|
||||||
|
获得像fwd的完美转发接受一个重载函数名或者模板函数名的方式是指定转发的类型。比如,你可以创造与f相同参数类型的函数指针,通过processVal或者workOnVal实例化这个函数指针(可以引导生成代码时正确选择函数实例),然后传递指针给f:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
using ProcessFuncType = int (*)(int); // make typedef; see Item 9
|
||||||
|
PorcessFuncType processValPtr = processVal; // specify needed signature for processVal
|
||||||
|
fwd(processValPtr); // fine
|
||||||
|
fwd(static_cast<ProcessFuncType>(workOnVal)); // alse fine
|
||||||
|
```
|
||||||
|
|
||||||
|
当然,这要求你知道fwd转发的函数指针的类型。对于完美转发来说这一点并不合理,毕竟,完美转发被设计为转发任何内容,如果没有文档告诉你转发的类型,你如何知道?(译者注:这里应该想表达,这是解决重载函数名或者函数模板的解决方案,但是这是完美转发本身的问题)
|
||||||
|
|
||||||
|
### 位域
|
||||||
|
|
||||||
|
完美转发最后一种失败的情况是函数参数使用位域这种类型。为了更直观的解释,IPv4的头部可以如下定义:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
struct IPv4Header {
|
||||||
|
std::uint32_t version:4,
|
||||||
|
IHL:4,
|
||||||
|
DSCP:6,
|
||||||
|
ECN:2,
|
||||||
|
totalLength:16;
|
||||||
|
...
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
如果声明我们的函数f(转发函数fwd的目标)为接收一个`std::size_t`的参数,则使用IPv4Header对象的totalLength字段进行调用没有问题:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
void f(std::size_t sz);
|
||||||
|
IPv4Header h;
|
||||||
|
...
|
||||||
|
f(h.totalLength);// fine
|
||||||
|
```
|
||||||
|
|
||||||
|
如果通过fwd转发h.totalLength给f呢,那就是一个不同的情况了:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
fwd(h.totalLength); // error!
|
||||||
|
```
|
||||||
|
|
||||||
|
问题在于fwd的参数是引用,而h.totalLength是非常量位域。听起来并不是那么糟糕,但是C++标准非常清楚地谴责了这种组合:非常量引用不应该绑定到位域。禁止的理由很充分。位域可能包含了机器字节的任意部分(比如32位int的3-5位),但是无法直接定位。我之前提到了在硬件层面引用和指针时一样的,所以没有办法创建一个指向任意bit的指针(C++规定你可以指向的最小单位是char),所以就没有办法绑定引用到任意bit上。
|
||||||
|
|
||||||
|
一旦意识到接收位域作为参数的函数都将接收位域的副本,就可以轻松解决位域不能完美转发的问题。毕竟,没有函数可以绑定引用到位域,也没有函数可以接受指向位域的指针(不存在这种指针)。这种位域类型的参数只能按值传递,或者有趣的事,常量引用也可以。在按值传递时,被调用的函数接受了一个位域的副本,而且事实表明,位域的常量引用也是将其“复制”到普通对象再传递。
|
||||||
|
|
||||||
|
传递位域给完美转发的关键就是利用接收参数函数接受的是一个副本的事实。你可以自己创建副本然后利用副本调用完美转发。在IPv4Header的例子中,可以如下写法:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// copy bitfield value; see Item6 for info on init. form
|
||||||
|
auto length = static_cast<std::uint16_t>(h.totalLength);
|
||||||
|
fwd(length); // forward the copy
|
||||||
|
```
|
||||||
|
|
||||||
|
### 总结
|
||||||
|
|
||||||
|
在大多数情况下,完美转发工作的很好。你基本不用考虑其他问题。但是当其不工作时,当看起来合理的代码无法编译,或者更糟的是,无法按照预期运行时,了解完美转发的缺陷就很重要了。同样重要的是如何解决它们。在大多数情况下,都很简单
|
||||||
|
|
||||||
|
### 需要记住的事
|
||||||
|
|
||||||
|
- 完美转发会失败当模板类型推导失败或者推导类型错误
|
||||||
|
- 导致完美转发失败的类型有braced initializers,作为空指针的0或者NULL,只声明的整型static const数据成员,模板和重载的函数名,位域
|
||||||
|
|
@ -1,4 +1,9 @@
|
|||||||
|
C++11的伟大标志之一是将并发整合到语言和库中。熟悉其他线程API(比如pthreads或者Windows threads)的开发者有时可能会对C++提供的斯巴达式(译者注:应该是简陋和严谨的意思)功能集感到惊讶,这是因为C++对于并发的大量支持是在编译器的约束层面。由此产生的语言保证意味着在C++的历史中,开发者首次通过标准库可以写出跨平台的多线程程序。这位构建表达库奠定了坚实的基础,并发标准库(tasks, futures, threads, mutexes, condition variables, atomic objects等)仅仅是成为并发软件开发者丰富工具集的基础。
|
||||||
|
|
||||||
|
在接下来的Item中,记住标准库有两个futures的模板:`std::future和std::shared_future`。在许多情况下,区别不重要,所以我们经常简单的混于一谈为*futures*。
|
||||||
|
|
||||||
# 优先基于任务编程而不是基于线程
|
# 优先基于任务编程而不是基于线程
|
||||||
|
|
||||||
如果开发者想要异步执行 `doAsyncWork` 函数,通常有两种方式。其一是通过创建 `std::thread` 执行 `doAsyncWork`, 比如
|
如果开发者想要异步执行 `doAsyncWork` 函数,通常有两种方式。其一是通过创建 `std::thread` 执行 `doAsyncWork`, 比如
|
||||||
```cpp
|
```cpp
|
||||||
int doAsyncWork();
|
int doAsyncWork();
|
||||||
|
120
7.The Concurrency API/item36.md
Normal file
120
7.The Concurrency API/item36.md
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
## Item 36:Specify std::launch::async if asynchronicity is essential
|
||||||
|
|
||||||
|
## Item36: 确保在异步为必须时,才指定`std::launch::async`
|
||||||
|
|
||||||
|
当你调用`std::async`执行函数时(或者其他可调用对象),你通常希望异步执行函数。但是这并不一定是你想要`std::async`执行的操作。你确实通过`std::async`launch policy(译者注:这里没有翻译)要求执行函数,有两种标准policy,都通过`std::launch`域的枚举类型表示(参见Item10关于枚举的更多细节)。假定一个函数**f**传给`std::async`来执行:
|
||||||
|
|
||||||
|
- **`std::launch::async`的launch policy**意味着f必须异步执行,即在不同的线程
|
||||||
|
- **`std::launch::deferred`的launch policy**意味着f仅仅在当调用`get或者wait`要求`std::async`的返回值时才执行。这表示f推迟到被求值才延迟执行(译者注:异步与并发是两个不同概念,这里侧重于惰性求值)。当`get或wait`被调用,f会同步执行,即调用方停止直到f运行结束。如果`get和wait`都没有被调用,f将不会被执行
|
||||||
|
|
||||||
|
有趣的是,`std::async`的默认launch policy是以上两种都不是。相反,是求或在一起的。下面的两种调用含义相同
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
auto fut1 = std::async(f); // run f using default launch policy
|
||||||
|
auto fut2 = std::async(std::launch::async | std::launch::deferred, f); // run f either async or defered
|
||||||
|
```
|
||||||
|
|
||||||
|
因此默认策略允许f异步或者同步执行。如同Item 35中指出,这种灵活性允许`std::async`和标准库的线程管理组件(负责线程的创建或销毁)避免超载。这就是使用`std::async`并发编程如此方便的原因。
|
||||||
|
|
||||||
|
但是,使用默认启动策略的`std::async`也有一些有趣的影响。给定一个线程t执行此语句:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
auto fut = std::async(f); // run f using default launch policy
|
||||||
|
```
|
||||||
|
|
||||||
|
- 无法预测f是否会与t同时运行,因为f可能被安排延迟运行
|
||||||
|
- 无法预测f是否会在调用`get或wait`的线程上执行。如果那个线程是t,含义就是无法预测f是否也在线程t上执行
|
||||||
|
- 无法预测f是否执行,因为不能确保`get或者wait`会被调用
|
||||||
|
|
||||||
|
默认启动策略的调度灵活性导致使用线程本地变量比较麻烦,因为这意味着如果f读写了线程本地存储(thread-local storage, TLS),不可能预测到哪个线程的本地变量被访问:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
auto fut = std::async(f); // TLS for f possibly for independent thread, but possibly for thread invoking get or wait on fut
|
||||||
|
```
|
||||||
|
|
||||||
|
还会影响到基于超时机制的wait循环,因为在task的`wait_for`或者`wait_until`调用中(参见Item 35)会产生延迟求值(`std::launch::deferred`)。意味着,以下循环看似应该终止,但是实际上永远运行:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
using namespace std::literals; // for C++14 duration suffixes; see Item 34
|
||||||
|
void f()
|
||||||
|
{
|
||||||
|
std::this_thread::sleep_for(1s);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto fut = std::async(f);
|
||||||
|
while (fut.wait_for(100ms) != std::future_status::ready)
|
||||||
|
{ // loop until f has finished running... which may never happen!
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
如果f与调用`std::async`的线程同时运行(即,如果为f选择的启动策略是`std::launch::async`),这里没有问题(假定f最终执行完毕),但是如果f是延迟执行,`fut.wait_for`将总是返回`std::future_status::deferred`。这表示循环会永远执行下去。
|
||||||
|
|
||||||
|
这种错误很容易在开发和单元测试中忽略,因为它可能在负载过高时才能显现出来。当机器负载过重时,任务推迟执行才最有可能发生。毕竟,如果硬件没有超载,没有理由不安排任务并发执行。
|
||||||
|
|
||||||
|
修复也是很简单的:只需要检查与`std::async`的future是否被延迟执行即可,那样就会避免进入无限循环。不幸的是,没有直接的方法来查看future是否被延迟执行。相反,你必须调用一个超时函数----比如`wait_for`这种函数。在这个逻辑中,你不想等待任何事,只想查看返回值是否`std::future_status::deferred`,如果是就使用0调用`wait_for`来终止循环。
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
auto fut = std::async(f);
|
||||||
|
if (fut.wait_for(0s) == std::future_status::deferred) { // if task is deferred
|
||||||
|
... // use wait or get on fut to call f synchronously
|
||||||
|
}
|
||||||
|
else { // task isn't deferred
|
||||||
|
while(fut.wait_for(100ms) != std::future_status::ready) { // infinite loop not possible(assuming f finished)
|
||||||
|
... // task is neither deferred nor ready, so do concurrent word until it's ready
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
这些各种考虑的结果就是,只要满足以下条件,`std::async`的默认启动策略就可以使用:
|
||||||
|
|
||||||
|
- task不需要和执行`get or wait`的线程并行执行
|
||||||
|
- 不会读写线程的线程本地变量
|
||||||
|
- 可以保证在`std::async`返回的将来会调用`get or wait`,或者该任务可能永远不会执行是可以接受的
|
||||||
|
- 使用`wait_for or wait_until`编码时考虑deferred状态
|
||||||
|
|
||||||
|
如果上述条件任何一个都满足不了,你可能想要保证`std::async`的任务真正的异步执行。进行此操作的方法是调用时,将`std::launch::async`作为第一个参数传递:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
auto fut = std::async(std::launch::async, f); // launch f asynchronously
|
||||||
|
```
|
||||||
|
|
||||||
|
事实上,具有类似`std::async`行为的函数,但是会自动使用`std::launch::async`作为启动策略的工具也是很容易编写的,C++11版本如下:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
template<typename F, typename... Ts>
|
||||||
|
inline
|
||||||
|
std::future<typename std::result_of<F(Ts...)>::type>
|
||||||
|
reallyAsync(F&& f, Ts&&... params)
|
||||||
|
{
|
||||||
|
return std::async(std::launch::async, std::forward<F>(f), std::forward<Ts>(params)...);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
这个函数接受一个可调用对象和0或多个参数params然后完美转发(参见Item25)给`std::async`,使用`std::launch::async`作为启动参数。就像`std::async`一样,返回`std::future`类型。确定结果的类型很容易,因为类型特征`std::result_of`可以提供(参见Item 9 关于类型特征的详细表述)。
|
||||||
|
|
||||||
|
`reallyAsync`就像`std::async`一样使用:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
auto fut = reallyAsync(f);
|
||||||
|
```
|
||||||
|
|
||||||
|
在C++14中,返回类型的推导能力可以简化函数的定义:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
template<typename f, typename... Ts>
|
||||||
|
inline
|
||||||
|
auto
|
||||||
|
reallyAsync(F&& f, Ts&&... params)
|
||||||
|
{
|
||||||
|
return std::async(std::launch::async, std::forward<T>(f), std::forward<Ts>(params)...);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
这个版本清楚表明,`reallyAsync`除了使用`std::launch::async`启动策略之外什么也没有做。
|
||||||
|
|
||||||
|
### 需要记住的事
|
||||||
|
|
||||||
|
- `std::async`的默认启动策略是异步或者同步的
|
||||||
|
- 灵活性导致访问**thread_locals**的不确定性,隐含了task可能不会被执行的意思,会影响程序基于`wait`的超时逻辑
|
||||||
|
- 只有确实异步时才指定`std::launch::async`
|
240
8.Tweaks/item42.md
Normal file
240
8.Tweaks/item42.md
Normal file
@ -0,0 +1,240 @@
|
|||||||
|
## Item42: 考虑使用emplacement代替insertion
|
||||||
|
|
||||||
|
如果你拥有一个容器,例如`std::string`,那么当你通过插入函数(例如`insert, push_front, push_back`,或者对于`std::forward_list`, `insert_after`)添加新元素时,你传入的元素类型应该是`std::string`。毕竟,这就是容器里的内容。
|
||||||
|
|
||||||
|
逻辑上看来如此,但是并非总是如此。考虑如下代码:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
std::vector<std::string> vs; // container of std::string
|
||||||
|
vs.push_back("xyzzy"); // add string literal
|
||||||
|
```
|
||||||
|
|
||||||
|
这里,容量里内容是`std::string`,但是你试图通过`push_back`加入字符串字面量,即引号内的字符序列。字符转字面量并不是`std::string`,这意味着你传递给`push_back`的参数并不是容器里的内容类型。
|
||||||
|
|
||||||
|
`std::vector`的`push_back`被按左值和右值分别重载:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
template<class T, class Allocator = allocator<T>>
|
||||||
|
class vector {
|
||||||
|
public:
|
||||||
|
...
|
||||||
|
void push_back(const &T x); // insert lvalue
|
||||||
|
void push_back(T&& x); // insert rvalue
|
||||||
|
...
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
在`vs.push_back("xyzzy")`这个调用中,编译器看到参数类型(const char[6])和`push_back`采用的参数类型(`std::string`的引用)之间不匹配。它们通过从字符串字面量创建一个`std::string`类型的临时变量来消除不匹配,然后传递临时变量给`push_back`。换句话说,编译器处理的这个调用应该像这样:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
vs.push_back(std::string("xyzzy")); // create temp std::string and pass it to push_back
|
||||||
|
```
|
||||||
|
|
||||||
|
代码编译并运行,皆大欢喜。除了对于性能执着的人意识到了这份代码不如预期的执行效率高。
|
||||||
|
|
||||||
|
为了创建`std::string`类型的临时变量,调用了`std::string`的构造器,但是这份代码并不仅调用了一次构造器,调用了两次,而且还调用了析构器。这发生在`push_back`运行时:
|
||||||
|
|
||||||
|
1. 一个`std::string`的临时对象从字面量"xyzzy"被创建。这个对象没有名字,我们可以称为*temp*,*temp*通过`std::string`构造器生成,因为是临时变量,所以*temp*是右值。
|
||||||
|
2. *temp*被传递给`push_back`的右值x重载函数。在`std::vector`的内存中一个x的副本被创建。这次构造器是第二次调用,在`std::vector`内部重新创建一个对象。(将x副本复制到`std::vector`内部的构造器是移动构造器,因为x传入的是右值,有关将右值引用强制转换为右值的信息,请参见Item25)。
|
||||||
|
3. 在`push_back`返回之后,*temp*被销毁,调用了一次`std::string`的析构器。
|
||||||
|
|
||||||
|
性能执着者(译者注:直译性能怪人)不禁注意到是否存在一种方法可以获取字符串字面量并将其直接传入到步骤2中的`std::string`内部构造,可以避免临时对象*temp*的创建与销毁。这样的效率最好,性能执着者也不会有什么意见了。
|
||||||
|
|
||||||
|
因为你是一个C++开发者,所以你会有高于平均水平的要求。如果你不是C++开发者,你可能也会同意这个观点(如果你根本不考虑性能,为什么你没在用python?)。所以让我来告诉你如何使得`push_back`达到最高的效率。就是不使用`push_back`,你需要的是`emplace_back`。
|
||||||
|
|
||||||
|
`emplace_back`就是像我们想要的那样做的:直接把传递的参数(无论是不是`std::string`)直接传递到`std::vector`内部的构造器。没有临时变量会生成:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
vs.emplace_back("xyzzy"); // construct std::string inside vs directly from "xyzzy"
|
||||||
|
```
|
||||||
|
|
||||||
|
`emplace_back`使用完美转发,因此只要你没有遇到完美转发的限制(参见Item30),就可以传递任何参数以及组合到`emplace_back`。比如,如果你在vs传递一个字符和一个数量给`std::string`构造器创建`std::string`,代码如下:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
vs.emplace_back(50, 'x'); // insert std::string consisting of 50 'x' characters
|
||||||
|
```
|
||||||
|
|
||||||
|
`emplace_back`可以用于每个支持`push_back`的容器。类似的,每个支持`push_front`的标准容器支持`emplace_front`。每个支持`insert`(除了`std::forward_list`和`std::array`)的标准容器支持`emplace。`关联容器提供`emplace_hint`来补充带有“hint”迭代器的插入函数,`std::forward_list`有`emplace_after`来匹配`insert_after`。
|
||||||
|
|
||||||
|
使得emplacement函数功能优于insertion函数的原因是它们灵活的接口。insertion函数接受对象来插入,而emplacement函数接受构造器接受的参数插入。这种差异允许emplacement函数避免临时对象的创建和销毁。
|
||||||
|
|
||||||
|
因为可以传递容器内类型给emplacement函数(该参数使函数执行复制或者移动构造器),所以即使insertion函数不会构造临时对象,也可以使用emplacement函数。在这种情况下,insertion和emplacement函数做的是同一件事,比如:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
std::string queenOfDisco("Donna Summer");
|
||||||
|
```
|
||||||
|
|
||||||
|
下面的调用都是可行的,效率也一样:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
vs.push_back(queenOfDisco); // copy-construct queenOfDisco
|
||||||
|
vs.emplace_back(queenOfDisco); // ditto
|
||||||
|
```
|
||||||
|
|
||||||
|
因此,emplacement函数可以完成insertion函数的所有功能。并且有时效率更高,至上在理论上,不会更低效。那为什么不在所有场合使用它们?
|
||||||
|
|
||||||
|
因为,就像说的那样,理论上,在理论和实际上没有什么区别,但是实际,区别还是有的。在当前标准库的实现下,有些场景,就像预期的那样,emplacement执行性能优于insertion,但是,有些场景反而insertion更快。这种场景不容易描述,因为依赖于传递的参数类型、容器类型、emplacement或insertion的容器位置、容器类型构造器的异常安全性和对于禁止重复值的容器(即`std::set,std::map,std::unorder_set,set::unorder_map`)要添加的值是否已经在容器中。因此,大致的调用建议是:通过benchmakr测试来确定emplacment和insertion哪种更快。
|
||||||
|
|
||||||
|
当然这个结论不是很令人满意,所以还有一种启发式的方法来帮助你确定是否应该使用emplacement。如果下列条件都能满足,emplacement会优于insertion:
|
||||||
|
|
||||||
|
- **值是通过构造器添加到容器,而不是直接赋值。**例子就像本Item刚开始的那样(添加"xyzzy"到`std::string的std::vector`中)。新值必须通过`std::string`的构造器添加到`std::vector`。如果我们回看这个例子,新值放到已经存在对象的位置,那情况就完全不一样了。考虑下:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
std::vector<std::string> vs; // as before
|
||||||
|
... // add elements to vs
|
||||||
|
vs.emplace(vs.begin(), "xyzzy"); // add "xyzzy" to beginning of vs
|
||||||
|
```
|
||||||
|
|
||||||
|
对于这份代码,没有实现会在已经存在对象的位置`vs[0]`构造添加的`std::string`。而是,通过移动赋值的方式添加到需要的位置。但是移动赋值需要一个源对象,所以这意味着一个临时对象要被创建,而emplacement优于insertion的原因就是没有临时对象的创建和销毁,所以当通过赋值操作添加元素时,emplacement的优势消失殆尽。
|
||||||
|
|
||||||
|
而且,向容器添加元素是通过构造还是赋值通常取决于实现者。但是,启发式仍然是有帮助的。基于节点的容器实际上总是使用构造器添加新元素,大多数标准库容器都是基于节点的。例外的容器只有`std::vector, std::deque, std::string`(`std::array`也不是基于节点的,但是它不支持emplacement和insertion)。在不是基于节点的容器中,你可以依靠`emplace_back`来使用构造向容器添加元素,对于`std::deque`,`emplace_front`也是一样的。
|
||||||
|
|
||||||
|
- **传递的参数类型与容器的初始化类型不同。**再次强调,emplacement优于insertion通常基于以下事实:当传递的参数不是容器保存的类型时,接口不需要创建和销毁临时对象。当将类型为T的对象添加到container<T>时,没有理由期望emplacement比insertion运行的更快,因为不需要创建临时对象来满足insertion接口。
|
||||||
|
|
||||||
|
- **容器不拒绝重复项作为新值。**这意味着容器要么允许添加重复值,要么你添加的元素都是不重复的。这样要求的原因是为了判断一个元素是否已经存在于容器中,emplacement实现通常会创建一个具有新值的节点,以便可以将该节点的值与现有容器中节点的值进行比较。如果要添加的值不在容器中,则链接该节点。然后,如果值已经存在,emplacement创建的节点就会被销毁,意味着构造和析构时浪费的开销。这样的创建就不会在insertion函数中出现。
|
||||||
|
|
||||||
|
本Item开始的例子中下面的调用满足上面的条件。所以调用比`push_back`运行更快。
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
vs.emplace_back("xyzzy"); // construct new value at end of container; don't pass the type in container; don't use container rejecting duplicates
|
||||||
|
vs.emplace_back(50, 'x'); // ditto
|
||||||
|
```
|
||||||
|
|
||||||
|
在决定是否使用emplacement函数时,需要注意另外两个问题。**首先**是资源管理。假定你有一个`std::shared_ptr<Widget>s`的容器,
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
std::list<std::shared_ptr<Widget>> ptrs;
|
||||||
|
```
|
||||||
|
|
||||||
|
然后你想添加一个通过自定义deleted释放的`std::shared_ptr`(参见Item 19)。Item 21说明你应该使用`std::make_shared`来创建`std::shared_ptr`,但是它也承认有时你无法做到这一点。比如当你要指定一个自定义deleter时。这时,你必须直接创建一个原始指针,然后通过`std::shared_ptr`来管理。
|
||||||
|
|
||||||
|
如果自定义deleter是这个函数,
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
void killWidget(Widget* pWidget);
|
||||||
|
```
|
||||||
|
|
||||||
|
使用insertion函数的代码如下:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
ptrs.push_back(std::shared_ptr<Widget>(new Widget, killWidget));
|
||||||
|
```
|
||||||
|
|
||||||
|
也可以像这样
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
ptrs.push_back({new Widget, killWidget});
|
||||||
|
```
|
||||||
|
|
||||||
|
不管哪种写法,在调用`push_back`中会生成一个临时`std::shared_ptr`对象。`push_back`的参数是`std::shared_ptr`的引用,因此必须有一个`std::shared_ptr`。
|
||||||
|
|
||||||
|
`std::shared_ptr`的临时对象创建应该可以避免,但是在这个场景下,临时对象值得被创建。考虑如下可能的时间序列:
|
||||||
|
|
||||||
|
1. 在上述的调用中,一个`std::shared_ptr<Widget> `的临时对象被创建来持有`new Widget`对象。称这个对象为*temp*。
|
||||||
|
2. `push_back`接受*temp*的引用。在节点的分配一个副本来复制*temp*的过程中,OOM异常被抛出
|
||||||
|
3. 随着异常从`push_back`的传播,*temp*被销毁。作为唯一管理Widget的弱指针`std::shared_ptr`对象,会自动销毁`Widget`,在这里就是调用`killWidget`。
|
||||||
|
|
||||||
|
这样的话,即使发生了异常,没有资源泄露:在调用`push_back`中通过`new Widget`创建的`Widget`在`std::shared_ptr`管理下自动销毁。生命周期良好。
|
||||||
|
|
||||||
|
考虑使用`emplace_back`代替`push_back`
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
ptrs.emplace_back(new Widget, killWidget);
|
||||||
|
```
|
||||||
|
|
||||||
|
1. 通过`new Widget`的原始指针完美转发给`emplace_back`的内部构造器。如果分配失败,还是抛出OOM异常
|
||||||
|
2. 当异常从`emplace_back`传播,原始指针是仅有的访问途径,但是因为异常丢失了,这就发生了资源泄露
|
||||||
|
|
||||||
|
在这个场景中,生命周期不良好,这个失误不能赖`std::shared_ptr`。`std::unique_ptr`使用自定义deleter也会有同样的问题。根本上讲,像`std::shared_ptr和std::unique_ptr`这样的资源管理类的有效性取决于资源被**立即**传递给资源管理对象的构造函数。实际上,这就是`std::make_shared和std::make_unique`这样的函数如此重要的原因。
|
||||||
|
|
||||||
|
在对存储资源管理类的容器调用insertion函数时(比如`std::list<std::shared_ptr<Widget>>`),函数的参数类型通常确保在资源的获取和管理资源对象的创建之间没有其他操作。在emplacement函数中,完美转发推迟了资源管理对象的创建,直到可以在容器的内存中构造它们为止,这给异常导致资源泄露提供了可能。所有的标准库容器都容易受到这个问题的影响。在使用资源管理对象的容器时,比如注意确保使用emplacement函数不会为提高效率带来降低异常安全性的后果。
|
||||||
|
|
||||||
|
坦白说,无论如何,你不应该将`new Widget`传递给`emplace_back或者push_back`或者大多数这种函数,因为,就像Item 21中解释的那样,这可能导致我们刚刚讨论的异常安全性问题。使用独立语句将从`new Widget`获取指针然后传递给资源管理类,然后传递这个对象的右值引用给你想传递`new Widget`的函数(Item 21 有这个观点的详细讨论)。代码应该如下:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
std::shared_ptr<Widget> spw(new Widget, killWidget); // create Widget and have spw manage it
|
||||||
|
ptrs.push_back(std::move(spw)); // add spw as rvalue
|
||||||
|
```
|
||||||
|
|
||||||
|
emplace_back的版本如下:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
std::shared_ptr<Widget> spw(new Widget, killWidget); // create Widget and have spw manage it
|
||||||
|
ptrs.emplace_back(std::move(spw));
|
||||||
|
```
|
||||||
|
|
||||||
|
无论哪种方式,都会产生spw的创建和销毁成本。给出选择emplacement函数优于insertion函数的动机是避免临时对象的开销,但是对于swp的概念来讲,当根据正确的方式确保获取资源和连接到资源管理对象上之间无其他操作,添加资源管理类型对象到容器中,emplacement函数不太可能胜过insertion函数。
|
||||||
|
|
||||||
|
emplacement函数的**第二个**值得注意的方面是它们与显式构造函数的交互。对于C++11正则表达式的支持,假设你创建了一个正则表达式的容器:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
std::vector<std::regex> regexes;
|
||||||
|
```
|
||||||
|
|
||||||
|
由于你同事的打扰,你写出了如下看似毫无意义的代码:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
regexes.emplace_back(nullptr); // add nullptr to container of regexes?
|
||||||
|
```
|
||||||
|
|
||||||
|
你没有注意到错误,编译器也没有提示你,所以你浪费了大量时间来调试。突然,你发现你插入了空指针到正则表达式的容器中。但是这怎么可能?指针不是正则表达式,如果你试图下面这样写
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
std::regex r = nullptr; // error! won't compile
|
||||||
|
```
|
||||||
|
|
||||||
|
编译器就会报错。有趣的是,如果你调用`push_back`而不是`emplace_back`,编译器就会报错
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
regexes.push_back(nullptr); // error! won't compile
|
||||||
|
```
|
||||||
|
|
||||||
|
当前你遇到的奇怪行为由于可能用字符串构造`std::regex`的对象,这就意味着下面代码合法:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
std::regex upperCaseWorld("[A-Z]+");
|
||||||
|
```
|
||||||
|
|
||||||
|
通过字符串创建`std::regex`要求相对较长的运行时开销,所以为了最小程度减少无意中产生此类开销的可能性,采用`const char*`指针的`std::regex`构造函数是显式的。这就是为什么下面代码无法编译的原因:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
std::regex r = nullptr; // error! won't compile
|
||||||
|
regexes.push_back(nullptr); // error
|
||||||
|
```
|
||||||
|
|
||||||
|
在上面的代码中,我们要求从指针到`std::regex`的隐式转换,但是显式构造的要求拒绝了此类转换。
|
||||||
|
|
||||||
|
但是在`emplace_back`的调用中,我们没有声明传递一个`std::regex`对象。代替的是,我们传递了一个`std::regex`构造器参数。那不是隐式转换,而是显式的:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
std::regex r(nullptr); // compiles
|
||||||
|
```
|
||||||
|
|
||||||
|
如果简洁的注释“compiles”表明缺乏直观理解,好的,因为这个代码可以编译,但是行为不确定。使用`const char*`指针的`std::regex`构造器要求字符串是一个有效的正则表达式,nullptr不是有效的。如果你写出并编译了这样的代码,最好的希望就是运行时crash掉。如果你不幸运,就会花费大量的时间调试。
|
||||||
|
|
||||||
|
先把`push_back, emplace_back`放在一边,注意到相似的初始化语句导致了多么不一样的结果:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
std::regex r1 = nullptr; // error ! won't compile
|
||||||
|
std::regex r2(nullptr); // compiles
|
||||||
|
```
|
||||||
|
|
||||||
|
在标准的官方术语中,用于初始化r1的语法是所谓的复制初始化。相反,用于初始化r2的语法是(也被称为braces)被称为直接初始化。复制初始化不是显式调用构造器的,直接初始化是。这就是r2可以编译的原因。
|
||||||
|
|
||||||
|
然后回到`push_back和 emplace_back`,更一般来说,insertion函数对比emplacment函数。emplacement函数使用直接初始化,这意味着使用显式构造器。insertion函数使用复制初始化。因此:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
regexes.emplace_back(nullptr); // compiles. Direct init permits use of explicit std::regex ctor taking a pointer
|
||||||
|
regexes.push_back(nullptr); // error! copy init forbids use of that ctor
|
||||||
|
```
|
||||||
|
|
||||||
|
要汲取的是,当你使用emplacement函数时,请特别小心确保传递了正确的参数,因为即使是显式构造函数,编译器可以尝试解释你的代码称为有效的(译者注:这里意思是即使你写的代码逻辑上不对,显式构造器时编译器可能能解释通过即编译成功)
|
||||||
|
|
||||||
|
### 需要记住的事
|
||||||
|
|
||||||
|
- 原则上,emplacement函数有时会比insertion函数高效,并且不会更差
|
||||||
|
- 实际上,当执行如下操作时,emplacement函数更快
|
||||||
|
1. 值被构造到容器中,而不是直接赋值
|
||||||
|
2. 传入的类型与容器类型不一致
|
||||||
|
3. 容器不拒绝已经存在的重复值
|
||||||
|
- emplacement函数可能执行insertion函数拒绝的显示构造
|
@ -42,12 +42,12 @@
|
|||||||
5. 右值引用,移动语意,完美转发
|
5. 右值引用,移动语意,完美转发
|
||||||
1. [Item 23:理解std::move和std::forward](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RvalueReferences_MovingSemantics_And_PerfectForwarding/item23.md) __由 @BlurryLight贡献__
|
1. [Item 23:理解std::move和std::forward](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RvalueReferences_MovingSemantics_And_PerfectForwarding/item23.md) __由 @BlurryLight贡献__
|
||||||
2. [Item 24:区别通用引用和右值引用](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RvalueReferences_MovingSemantics_And_PerfectForwarding/item24.md) __由 @BlurryLight贡献__
|
2. [Item 24:区别通用引用和右值引用](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RvalueReferences_MovingSemantics_And_PerfectForwarding/item24.md) __由 @BlurryLight贡献__
|
||||||
3. Item 25:对于右值引用使用std::move,对于通用引用使用std::forward
|
3. [Item 25:对于右值引用使用std::move,对于通用引用使用std::forward](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RvalueReferences_MovingSemantics_And_PerfectForwarding/item25.md)__由 @wendajiang贡献__
|
||||||
4. Item 26:避免重载通用引用
|
4. Item 26:避免重载通用引用
|
||||||
5. Item 27:熟悉重载通用引用的替代品
|
5. Item 27:熟悉重载通用引用的替代品
|
||||||
6. Item 28:理解引用折叠
|
6. [Item 28:理解引用折叠](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RvalueReferences_MovingSemantics_And_PerfectForwarding/item28.md)__由 @wendajiang贡献__
|
||||||
7. [Item 29:认识移动操作的缺点](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RvalueReferences_MovingSemantics_And_PerfectForwarding/item29.md) __由 @wendajiang贡献__
|
7. [Item 29:认识移动操作的缺点](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RvalueReferences_MovingSemantics_And_PerfectForwarding/item29.md) __由 @wendajiang贡献__
|
||||||
8. Item 30:熟悉完美转发失败的情况
|
8. [Item 30:熟悉完美转发失败的情况](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RvalueReferences_MovingSemantics_And_PerfectForwarding/item30.md)__由 @wendajiang贡献__
|
||||||
6. Lambda表达式
|
6. Lambda表达式
|
||||||
1. [Item 31:避免使用默认捕获模式](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/6.Lambda%20Expressions/item31.md) __由 @LucienXian贡献__
|
1. [Item 31:避免使用默认捕获模式](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/6.Lambda%20Expressions/item31.md) __由 @LucienXian贡献__
|
||||||
2. [Item 32:使用初始化捕获来移动对象到闭包中](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/6.Lambda%20Expressions/item32.md) __由 @LucienXian贡献__
|
2. [Item 32:使用初始化捕获来移动对象到闭包中](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/6.Lambda%20Expressions/item32.md) __由 @LucienXian贡献__
|
||||||
@ -62,7 +62,7 @@
|
|||||||
6. Item 40:对于并发使用std::atomic,volatile用于特殊内存区
|
6. Item 40:对于并发使用std::atomic,volatile用于特殊内存区
|
||||||
8. 微调
|
8. 微调
|
||||||
1. [Item 41:对于那些可移动总是被拷贝的形参使用传值方式](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/8.Tweaks/item41.md) __由 @wendajiang贡献__
|
1. [Item 41:对于那些可移动总是被拷贝的形参使用传值方式](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/8.Tweaks/item41.md) __由 @wendajiang贡献__
|
||||||
2. Item 42:考虑就地创建而非插入
|
2. [Item 42:考虑就地创建而非插入](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/8.Tweaks/item42.md) __由 @wendajiang贡献__
|
||||||
|
|
||||||
## 贡献者
|
## 贡献者
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user