Update item25.md

This commit is contained in:
猫耳堀川雷鼓 2021-02-28 21:31:15 +08:00 committed by GitHub
parent 65fc362037
commit 8870e30a58
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

View File

@ -40,7 +40,7 @@ public:
};
```
总而言之,当把右值引用转发给其他函数时,右值引用应该**无条件转换**为右值(通过`std::move`),因为它们**总是**绑定到右值;当转发时,通用引用应该有条件转换为右值(通过`std::forward`),因为它们只是**有时**绑定到右值。
总而言之,当把右值引用转发给其他函数时,右值引用应该**无条件转换**为右值(通过`std::move`),因为它们**总是**绑定到右值;当转发通用引用时,通用引用应该**有条件转换**为右值(通过`std::forward`),因为它们只是**有时**绑定到右值。
[Item23](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item23.md)解释说,可以在右值引用上使用`std::forward`表现出适当的行为,但是代码较长,容易出错,所以应该避免在右值引用上使用`std::forward`。更糟的是在通用引用上使用`std::move`,这可能会意外改变左值(比如局部变量):
@ -70,7 +70,7 @@ w.setName(n); //把n移动进w
上面的例子,局部变量`n`被传递给`w.setName`,调用方可能认为这是对`n`的只读操作——这一点倒是可以被原谅。但是因为`setName`内部使用`std::move`无条件将传递的引用形参转换为右值,`n`的值被移动进`w.name`,调用`setName`返回时`n`最终变为未定义的值。这种行为使得调用者蒙圈了——还有可能变得狂躁。
你可能争辩说`setName`不应该将其形参声明为通用引用。此类引用不能使用`const`(见[Item24](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item24.md)),但是`setName`肯定不应该修改其形参可能会指出,如果为`const`左值和为右值分别重载`setName`可以避免整个问题,比如这样:
你可能争辩说`setName`不应该将其形参声明为通用引用。此类引用不能使用`const`(见[Item24](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RRefMovSemPerfForw/item24.md)),但是`setName`肯定不应该修改其形参。你可能会指出,如果为`const`左值和为右值分别重载`setName`可以避免整个问题,比如这样:
```cpp
class Widget {
@ -125,7 +125,7 @@ void setSignText(T&& text) //text是通用引用
对于`std::move`,同样的思路(即最后一次用右值引用的时候再调用`std::move`),但是需要注意,在有些稀少的情况下,你需要调用`std::move_if_noexcept`代替`std::move`。要了解何时以及为什么,参考[Item14](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/3.MovingToModernCpp/item14.md)。
如果你在**按值**返回的函数中,返回值绑定到右值引用或者通用引用上,需要对返回的引用使用`std::move`或者`std::forward`。要了解原因,考虑两个矩阵相加的`operator+`函数,左侧的矩阵参数为右值(可以被用来保存求值之后的和):
如果你在**按值**返回的函数中,返回值绑定到右值引用或者通用引用上,需要对返回的引用使用`std::move`或者`std::forward`。要了解原因,考虑两个矩阵相加的`operator+`函数,左侧的矩阵为右值(可以被用来保存求值之后的和):
```cpp
Matrix //按值返回
@ -191,7 +191,7 @@ Widget makeWidget() //makeWidget的移动版本
这是错的,因为对于这种优化,标准化委员会远领先于开发者。早就为人认识到的是,`makeWidget`的“拷贝”版本可以避免复制局部变量`w`的需要,通过在分配给函数返回值的内存中构造`w`来实现。这就是所谓的**返回值优化***return value optimization*RVO这在C++标准中已经实现了。
对这种好事遣词表达是个讲究活,因为你想只在那些不影响软件外在行为的地方允许这样的**拷贝消除**copy elision。对标准中教条的也可以说是有毒的絮叨做些解释这个特定的好事就是说编译器可能会在按值返回的函数中消除对局部对象的拷贝或者移动如果满足1局部对象与函数返回值的类型相同2局部对象就是要返回的东西。适合的局部对象包括大多数局部变量比如`nakeWidget`里的`w`),还有作为`return`语句的一部分而创建的临时对象。函数形参不满足要求。一些人将RVO的应用区分为命名的和未命名的即临时的局部对象限制了RVO术语应用到未命名对象上并把对命名对象的应用称为**命名返回值优化***named return value optimization*NRVO把这些记在脑子里再看看`makeWidget`的“拷贝”版本:
对这种好事遣词表达是个讲究活,因为你想只在那些不影响软件外在行为的地方允许这样的**拷贝消除**copy elision。对标准中教条的也可以说是有毒的絮叨做些解释这个特定的好事就是说编译器可能会在按值返回的函数中消除对局部对象的拷贝或者移动如果满足1局部对象与函数返回值的类型相同2局部对象就是要返回的东西。适合的局部对象包括大多数局部变量比如`makeWidget`里的`w`),还有作为`return`语句的一部分而创建的临时对象。函数形参不满足要求。一些人将RVO的应用区分为命名的和未命名的即临时的局部对象限制了RVO术语应用到未命名对象上并把对命名对象的应用称为**命名返回值优化***named return value optimization*NRVO把这些记在脑子里再看看`makeWidget`的“拷贝”版本:
```cpp
Widget makeWidget() //makeWidget的“拷贝”版本
@ -212,7 +212,7 @@ return std::move(w);
返回的已经不是局部对象`w`,而是**`w`的引用**——`std::move(w)`的结果。返回局部对象的引用不满足RVO的第二个条件所以编译器必须移动`w`到函数返回值的位置。开发者试图对要返回的局部变量用`std::move`帮助编译器优化,反而限制了编译器的优化选项。
但是RVO就是个优化。编译器不被**要求**消除拷贝和移动操作,及时他们被允许这样做。或许你会疑惑并担心编译器用拷贝操作惩罚你因为它们确实可以这样。或者你可能有足够的了解意识到有些情况很难让编译器实现RVO比如当函数不同控制路径返回不同局部变量时。编译器必须产生一些代码在分配的函数返回值的内存中构造适当的局部变量但是编译器如何确定哪个变量是合适的呢如果这样你可能会愿意以移动的代价来保证不会产生拷贝。那就是极可能仍然认为应用`std::move`到一个要返回的局部对象上是合理的,只因为可以不再担心拷贝的代价。
但是RVO就是个优化。编译器不被**要求**消除拷贝和移动操作,即使他们被允许这样做。或许你会疑惑并担心编译器用拷贝操作惩罚你因为它们确实可以这样。或者你可能有足够的了解意识到有些情况很难让编译器实现RVO比如当函数不同控制路径返回不同局部变量时。编译器必须产生一些代码在分配的函数返回值的内存中构造适当的局部变量但是编译器如何确定哪个变量是合适的呢如果这样你可能会愿意以移动的代价来保证不会产生拷贝。那就是极可能仍然认为应用`std::move`到一个要返回的局部对象上是合理的,只因为可以不再担心拷贝的代价。
那种情况下,应用`std::move`到一个局部对象上**仍然**是一个坏主意。C++标准关于RVO的部分表明如果满足RVO的条件但是编译器选择不执行拷贝消除则返回的对象**必须被视为右值**。实际上标准要求当RVO被允许时或者实行拷贝消除或者将`std::move`隐式应用于返回的局部对象。因此,在`makeWidget`的“拷贝”版本中,
@ -225,7 +225,7 @@ Widget makeWidget() //同之前一样
}
```
编译器要不消除`w`的拷贝,要不函数看成像下面写的一样:
编译器要不消除`w`的拷贝,要不函数看成像下面写的一样:
```cpp
Widget makeWidget()