条款 25 - 针对右值引用实施 std move, 针对万能引用实施 std forward

ZaynPei Lv6

我们在条款23了解了std::move 与 std::forward, 而条款25提供了一个简单、强大且几乎永远正确的指导方针,用于决定何时使用 std::move 以及何时使用 std::forward, 那就是: - 对右值引用,总是使用 std::move 来进行转发。 - 对万能引用,总是使用 std::forward 来进行转发。

std::move 用于右值引用

右值引用(如 Widget&&)在形参中有一个明确的特性:它只能绑定到右值,即那些可以被移动的对象。虽然形参本身(例如 rhs)是一个左值,但我们确切地知道它所引用的对象是临时的或被显式标记为可移动的。

因此,当我们要将这个形参传递给其他函数(例如,在构造函数中初始化成员)时,我们应该无条件地将其转换为右值,以触发移动操作。std::move 就是执行这种无条件转换的工具。

如下, 在移动构造函数中,形参 rhs 是一个右值引用。我们使用 std::move 来移动其成员 name 和 p 到当前对象的成员中。

1
2
3
4
5
6
7
8
9
10
class Widget {
public:
Widget(Widget&& rhs) // rhs 是右值引用
: name(std::move(rhs.name)), // 对 rhs 的成员实施移动
p(std::move(rhs.p))
{ ... }
private:
std::string name;
std::shared_ptr<SomeDataStructure> p;
};

这个指导方针也适用于按值返回的函数。当你在 return 语句中返回一个绑定到右值引用形参对象时,应该相应地使用 std::move 来避免拷贝, 提升效率。

1
2
3
4
Matrix operator+(Matrix&& lhs, const Matrix& rhs) {
lhs += rhs;
return std::move(lhs); // 将 lhs 移动到返回值中
}

然而, 如果你的 return 语句中返回的是局部变量, 千万不要对局部变量使用 std::move, 例如这样的代码

1
2
3
4
5
Widget makeWidget() {
Widget w;
// ...
return std::move(w); // 错误的做法!
}
上述两者之所以不同, 是因为 C++ 编译器具有返回值优化(Return Value Optimization, RVO) 机制。对于 return w; 这样的语句,编译器通常可以直接在为函数返回值分配的内存中构造 局部变量w,从而完全避免任何复制或移动操作。同时, C++标准规定,在 return 语句中,局部变量会被自动视为右值。这意味着 return w; 的行为等同于 return std::move(w);

如果使用 std::move(w) 会将 w 转换为右值引用。返回一个局部对象的引用会阻止编译器执行 RVO。因此,添加 std::move 不仅没有好处,反而可能阻止一项重要的编译器优化,弄巧成拙, 导致性能下降。

而返回形参时, 由于形参不是函数内部创建的局部对象, 被视为左值,且不受RVO规则的约束。为了触发移动,你必须使用 std::move

std::forward 用于万能引用

万能引用(在模板中声明为 T&&)则不同,它既可以绑定到右值,也可以绑定到左值。我们需要在转发它时保留其原始的左/右值属性。如果原始实参是右值,我们就应该将其作为右值转发;如果原始实参是左值,我们就应该将其作为左值转发 。

因此, std::forward 正是执行这种有条件的转换的工具。它会检查模板参数 T 的推导类型,并仅在原始实参是右值的情况下,才将形参转换为右值。

如下,在一个接受万能引用的 setName 函数中,我们使用 std::forward 来将 newName 转发给成员 name 的赋值运算符, 从而确保了当 setName 接收到右值时,会触发 std::string 的移动赋值;而当接收到左值时,则触发复制赋值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Widget {
public:
template<typename T>
void setName(T&& newName) { // newName 是万能引用
name = std::forward<T>(newName); // 转发 newName
}
// ...
};

std::string getWidgetName(); // 工厂函数,返回右值
Widget w;
w.setName(getWidgetName()); // 调用时传入右值,setName 内部发生移动赋值

auto n = getWidgetName();
w.setName(n); // 调用时传入左值 n,setName 内部发生复制赋值
> 如果在万能引用上误用 std::move,将导致一个严重的bug:它会无条件地将形参转换为右值,即使调用者传入的是一个左值。这会导致调用者的局部变量被意外“掏空”

同理, 当你在 return 语句中返回一个绑定到万能引用形参的对象时,应该相应地使用 std::forward。这里的情况与上述的std::move一致

1
2
3
4
5
6
7
template<typename T>
Fraction reduceAndCopy(T&& frac) // frac 是一个万能引用
{
frac.reduce();
// 关键:按值返回,但使用 std::forward 转发 frac
return std::forward<T>(frac);
}

On this page
条款 25 - 针对右值引用实施 std move, 针对万能引用实施 std forward