move与forward复习
右值引用绑定至可移动的对象,如果你当前持有一个右值引用参数,你应当明确该对象具备移动特性:
1 | class Widget { |
为了充分利用对象的移动特性,我们可以利用std::move将上述移动构造定义为:1
2
3
4
5
6
7
8class Widget {
public:
Widget(Widget&& rhs):name(std::move(rhs.name)),p(std::move(rhs.p)){ … }
…
private:
std::string name;
std::shared_ptr<SomeDataStructure> p;
};
universal reference也可绑定至可移动的对象,为了确保它能够正常运转,一般而言需要对它执行std::forward操作:1
2
3
4
5
6
7
8class Widget {
public:
template<typename T>
void setName(T&& newName){ // name is a universal reference
name = std::forward<T>(newName);
}
…
};
简而言之,右值引用在转发至其他函数时应当被无条件地转为右值(使用move),而universal reference则需要有条件地转为右值(使用forward),因为它们仅有在被右值初始化时才能体现出右值特性。
问题实例
在Item23中曾经提及到forward亦作用于右值,只是使用起来较为繁琐(需要手动声明类型),但相对于繁琐,将move作用于universal reference则可能会导致雪崩,因为它可能会修改某个左值(比如说强行移动了某个局部变量):
1 | class Widget { |
当然,你也可能会争辩说这个程序设计存在问题,setName不会修改参数,因此应当具备const属性从而规避universal reference,正确的写法应当是针对const左值与可被移动的右值作出重载:1
2
3
4
5
6
7
8
9
10class Widget {
public:
void setName(const std::string& newName){
name = newName;
}
void setName(std::string&& newName){
name = std::move(newName);
}
…
};
我们可以认为重载版本存在3个缺陷:
- 较多的源代码
- 性能较低
- 较低的扩展性
缺陷1不言自明,下面重点讨论2与3。
性能分析
假设当前存在调用如下:1
w.setName("Adela Novak");
若采用universal reference版本,字符串“Adela Novak”(此时类型是const char* 而非string)将被传递给setName后直接作为string赋值运算符参数,因此,w的name数据成员将直接经由该字符串赋值,不会出现任何临时对象。但重载版本将为setName的参数创建一个临时的std::string对象(因为常量字符串并非string类型),然后将此临时std::string移动到w的数据成员中。因此,对setName的调用将需要执行一个std::string构造函数(用于创建临时对象),一个std::string移动赋值运算符(将newName移动到w.name),以及一个std::string析构函数(用于析构临时对象),这一系列操作必然花费不菲。
扩展性分析
setName只存在一个参数,重载版本需要给出两个版本,显然函数参数一旦增长,重载函数的数量将以指数级别增长,此外,某些函数的参数甚至是无限数量,其中的典型案例即为std::make_shared与std::make_unique,它们的声明分别是:1
2
3
4template<class T, class... Args>
shared_ptr<T> make_shared(Args&&... args);
template<class T, class... Args>
unique_ptr<T> make_unique(Args&&... args);
此时必须使用universal reference,而forward是它的最佳拍档。
move与forward的使用时机
在某些情况下,我们可能会在单个函数中多次使用绑定到右值引用或universal reference的对象,在此情况下,我们需要保证在结束使用前该对象不会被移动。此时,我们只能在最后一次使用中使用move(右值引用)或forward(universal reference):1
2
3
4
5
6template<typename T>
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
}
move使用与之同理,只是有时你需要使用move_if_noexcept,具体原因可见Item14。
move、forward与函数返回值
若一个函数需要返回一个绑定至右值引用或universal reference的对象,则在返回语句中需要使用move或forward。
move
试以矩阵加法举例:1
2
3
4Matrix operator+(Matrix&& lhs, const Matrix& rhs){
lhs += rhs;
return std::move(lhs); // move lhs into return value
}
如果我们忽视对move的调用:1
2
3
4Matrix operator+(Matrix&& lhs, const Matrix& rhs){
lhs += rhs;
return lhs; // copy lhs into return value
}
由于lhs是一个左值,因此编译器只能执行copy操作,这对性能影响较大。就算该类型不支持移动构造,最坏结果也无非就是执行一次copy而已,不会引发任何错误。
forward
forward类似于move。考虑现有一个函数模板reduceAndCopy,它接受一个可能未减少的Fraction对象,函数负责减少它,然后返回减少值的副本(看起来像是后缀自减)。如果原始对象是右值,则应将其值移入返回值(从而避免制作副本的费用),如果原始值为左值,则必须创建副本:1
2
3
4
5template<typename T>
Fraction reduceAndCopy(T&& frac){
frac.reduce();
return std::forward<T>(frac); // move rvalue into return value, copy lvalue
}
如果不采用forward,则将无条件执行复制操作。
不适用场景(返回局部变量)
有些开发者可能会刻舟求剑,将上述使用范例推广至不适用之处,他们可能会作出如下判断:“如果利用std::move可以直接移动构建返回值,那返回局部变量时可能亦可使用”,于是他们写出这样的代码:1
2
3
4
5Widget makeWidget(){
Widget w; // local variable
… // configure w
return std::move(w); // move w into return value
}
这种行为并不可取,应当采用RVO加以改进(见More Effective C++ Item20)。
总结
- 将move应用于右值引用,forward应用于universal refernce。
- 当需要返回非局部对象时,采用建议1。
- 返回局部对象时使用RVO。