explorer

万丈高楼平地起,勿在浮沙筑高台

0%

Effective C++ :对右值引用使用 std::move,对通用引用使用 std::forward

当形参被声明为右值引用时,意味着传入的实参需要是右值引用,并且该参数是可移动的。既然目的如此明确,那么使用std::move是正确的选择:

1
2
3
4
5
6
7
8
9
10
11
class Widget {
public:
Widget(Widget&& rhs) // rhs is rvalue reference
: name(std::move(rhs.name)),
p(std::move(rhs.p))
{ … }

private:
std::string name;
std::shared_ptr<SomeDataStructure> p;
};
但如果形参被声明为通用引用时,则意味着实参有可能是右值引用,该参数有可能可被移动。那么对应的使用std::forward是正确的选择:
1
2
3
4
5
6
7
class Widget {
public:
template<typename T>
void setName(T&& newName) // newName is
{ name = std::forward<T>(newName); } // universal reference

};

如果将std::move应用于通用引用会怎样?

如下代码所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <vector>
#include <iostream>
#include <array>
#include <memory>

class Widget {
public:
template<typename T>
void setName(T&& newName) // universal reference
{ name = std::move(newName); } // compiles, but is
// bad, bad, bad!
private:
std::string name;
};

int main(void) {

std::string str = {"hello,world\n"};

std::cout << "The contents of str before move : " << str;

Widget widget{};

widget.setName(str);

std::cout << "The contents of str after move : " << str;


return 0;
}

其输出为:

The contents of str before move : hello,world The contents of str after move :

作为widget的使用者,其本意是将str的内容拷贝一份给widget,但是由于使用了std::move进行无条件转换。最终str指向的内容被移动到了widget的私有成员name中。

但作为使用者还以为str中依然是原来的内容,如果继续操作str便会遇到未定义的错误!

对返回值使用std::movestd::forward

好心办坏事

同时满足以下两个条件时,c++ 会应用返回值优化策略(return value optimization,RVO):

  1. 局部变量的类型和函数的返回类型一致
  2. 当前局部变量就是被返回的变量

那么编译器会尝试使用移动语义,以提高返回变量的效率。

假设在满足 RVO 的前提下,用户主动使用std::move转换返回的变量会怎么样?

1
2
3
4
5
6
Widget makeWidget()        
{
Widget w;

return std::move(w);
}

其实这是会起反作用的,因为使用std::move转换后,返回的类型便是Widget的右值引用了,这反而影响了编译器的判断(不满足条件 2)而最终使用拷贝的方式返回。

相当于用户手动的实现了移动操作,而绕过了编译器的自主优化。

正确的使用场景

那么哪种情况下比较适合主动使用std::movestd::forward呢?

也就是返回的变量不满足上面两个条件时,可以主动使用移动语义:

1
2
3
4
5
6
Matrix                                        // by-value return
operator+(Matrix&& lhs, const Matrix& rhs)
{
lhs += rhs;
return std::move(lhs); // move lhs into
} // return value

上面这个+重载函数是以值的方式返回,参数lhs是一个通用引用。

这种情况下使用std::move转换一下,编译器便会尝试使用移动语义来提高效率:

  • 如果Matrix类支持移动操作,那么就会调用移动操作。
  • 如果Matrix类不支持移动操作,那么就会使用拷贝操作
1
2
3
4
5
6
7
template<typename T>         
Fraction // by-value return
reduceAndCopy(T&& frac) // universal reference param
{
frac.reduce();
return std::forward<T>(frac); // move rvalue into return
} // value, copy lvalue

当参数frac是左值时,那么就使用拷贝操作。

当参数frac是右值时,则将其转换为右值引用,并尝试使用移动操作。