Item 25: Use std::move on rvalue references, std::forward on universal references.
Effective Modern C++ Item 25 的学习和解读。
如果函数参数为右值引用,那么这个参数只能绑定到一个右值,你应该充分利用右值的特性(可以被移动),使用 std::move 无条件将参数转换为右值。
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 将其转换为右值。
class Widget {
public:
template<typename T>
void setName(T&& newName) // newName is universal reference
{ name = std::forward<T>(newName); }
…
};
总的来说,在转发右值引用参数给其他函数时候,应该使用 std::move 无条件将其转为右值。当转发万能引用参数给其他函数时候,应该使用 std::forward 有条件将其转换为右值,因为万能引用有可能绑定到右值。
虽然参数是右值引用时候,使用 std::forward 会将其转换为右值,但还是建议你使用 std::move,因为这样代码更加简洁,也更符合习惯。
如果参数是万能引用,则需要避免使用 std::move 转换为右值。看下面的例子:
class Widget {
public:
template<typename T>
void setName(T&& newName) // universal reference
{ name = std::move(newName); }
…
private:
std::string name;
std::shared_ptr<SomeDataStructure> p;
};
std::string getWidgetName(); // factory function
Widget w;
auto n = getWidgetName(); // n is local variable
w.setName(n); // moves n into w!
… // n's value now unknown
这里使用 std::move 将会无条件将参数转为为右值,n 会被移动给 w.name,n 会变空,这显然不是好的代码设计。为了让 setName 函数不修改入参,有人可能会想通过重载 setName 改善上面代码:
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); }
…
};
这依然不是好的设计,还是有缺点。一方面,上面的代码可能比较低效,考虑这样的调用:
w.setName("Adela Novak");
std::string 是可以直接通过字面字符串进行构造,如果是万能引用版本,则可以直接在 setName 内部通过字面字符串直接构造 w.name。但是对于重载版本的 setName 来说,则会产生临时的 std::string 对象。
另一方面,最大的缺点是 setName 的参数若有 N 个的话,那需要写 2^N 个重载函数。更糟糕的是,像模板函数不限制个数的参数时候,这种重载的方式更难以为继了。
template<class T, class... Args> // from C++11
shared_ptr<T> make_shared(Args&&... args); // Standard
template<class T, class... Args> // from C++14
unique_ptr<T> make_unique(Args&&... args); // Standard
需要注意的是,当我们在一个函数中使用 std::move 转换右值引用和 std::forward 转化万能引用时候,在这个参数最后一次使用时候才应用 std::move 或 std::forward 。
template<typename T> // text is
void setSignText(T&& text) // univ. reference
{
sign.setText(text); // use text, but don't modify it
auto now = std::chrono::system_clock::now(); // get current time
signHistory.add(now, std::forward<T>(text)); // conditionally cast text to rvalue
}
如果函数的入参是一个右值引用(或万能引用),函数体中返回这个入参(by value),你应该使用 std::move (std::forward) 来返回这个引用。
Matrix // by-value return
operator+(Matrix&& lhs, const Matrix& rhs)
{
lhs += rhs;
return std::move(lhs); // move lhs into return value
}
使用 std::move 将 lhs 转化为右值,可以促使编译使用移动而非拷贝的方式将 lhs 移动给函数返回值。
对于万能引用,情况也是类似的。如果参数绑定到右值,使用 std::forward 可以促使编译器使用移动而非拷贝动作。
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
但是,上述的情况不能推广到函数中返回局部变量的场景。看下面的例子:
Widget makeWidget() // "Copying" version of makeWidget
{
Widget w; // local variable configure w
…
return w; // "copy" w into return value
}
你可能做如下 “优化” :
Widget makeWidget() // Moving version of makeWidget
{
Widget w;
…
return std::move(w); // move w into return value
} // (don't do this!)
“优化” 的版本反而会让编译器生成的代码效率更低,原因是因为编译器的返回值优化(RVO),可以查阅 C++ 返回值优化 RVO 了解更多,这里不再赘述了。