首先要理解 std::move 和 std::forward 需要对 C++ 中的左值和右值有一个了解,可以参考:
C++ 中的左值与右值
一般在 C++ 中的移动语义对应的是 std::move ,完美转发对应的是 std::forward。 但是不论是所谓的移动,还是转发,其实代码并没有移动或者转发任何东西,它们唯一做的就是强制类型转换。 std::move 做的是无条件的将实参强制转换成右值,而 std::forward 则仅在某个特定的条件下才强制转换。
std::move
template<typename T>
typename std::remove_reference<T>::T&& move(T&& param) { // 这里的 T&& 是万能引用
using ReturnType = typepename std::remove_reference<T>::type&&;
return static_cast<Returnype>(param);
}
这里使用了 std::remove_reference<T> 的作用就是把 T& 或 T&& 全部变成 T,从而保证 std::move 返回的一定是右值引用。但是变成右值引用,不代表这一定可以移动。
主要原因还是在 C++ 中的左值与右值 中对 const/非const 的左值引用/右值引用,这 4 种引用相互之间可以绑定的情况。
class Blob{
public:
explict Blob(std::string text); // 这里形参 text 按值传递
...
}
如果 Blob 的构造函数只是对 text 进行读取而不是修改,理论上我们更应该加上 const 修饰词:
class Blob{
public:
explict Blob(const std::string text); // 这里形参 text 按值传递
...
为了避免 text 复制进数据成员的过程产生昂贵的复制操作,我们使用移动语义 std::move,产生一个右值:
class Blob{
public:
explict Blob(const std::string text):value(std::move(text)){...}
...
private:
std::string value;
上述代码可以正常经过编译并运行,但是并没有被移动进 value,而是仍然被复制进 value 的。
原因在于,当我们给变量加上 const 修饰词以后,std::move 出来的是 const 右值引用。而编译器面的选择有:
class string{
public:
string(const string& rhs); // 复制构造函数
string(string&& rhs); // 移动构造函数
}
C++ 中的左值与右值 中说明了,这里的 string 移动构造函数参数是 非 const 右值引用,而非 const 右值引用只能绑定非 const 右值引用,我们传入的是 const 右值引用,但是 const 右值引用是允许绑定在 const 左值引用上的,因此编译器最终还是会选择复制构造函数而不是我们期望的移动构造函数。
综上:
- 如果想某个对象拥有可以执行移动操作的能力,就不要将其声明成 const
- std::move 仅仅保证输出结果是右值,不实际移动任何东西,甚至不保证强制类型以后的对象具备可移动的能力
std::forward
template<typename T>
T&& forward(typename tinySTL::remove_reference<T>::type& t) noexcept {
return static_cast<T&&>(t);
template<typename T>
T&& forward(typename tinySTL::remove_reference<T>::type&& t) noexcept {
return static_cast<T&&>(t);
}
当 T 是左值引用时,推导得 T 是 T&, T& &&,折叠以后还是 T&,同理 T 是右值引用时,T 是 T&&,T&& &&,折叠以后是 T&&。
void process(const Blob& lv); // 处理左值
void process(Blob&& rv); // 处理右值
template<typename T>
void logAndProcess(T&& param){
process(std::forward<T>(param));
}
上述情形,我们是期望传入的 param 是左值则调用左值处理,是右值则调用右值处理。但是所有函数的形参都是左值,所以所有对 process 的调用都是左值情况,为了避免这种情况,需要对实例化 param 的实参在必要时进行右值转换,就可以用 std::forward。
使用小结
std::move 和 std::forward 都是强制类型类型转换,std::move 是总是会发生,std::forward 则是满足条件才会发生,那么用 std::forward 是不是就可以了?答案是肯定的,但是 std::move 存在的意义主要可以是方便,减少错误可能,代码更加清晰。且在运行期,std::move 和 std::forward 不会做任何操作。
我们可以针对右值引用使用 std::move,针对左值引用使用 std::forward:
// 右值引用
class widget{
public:
Widget(Widget&& rhs):name(std::move(rhs.name)){...} // rhs 是右值引用
...
}
// 万能引用
class widget{
public:
template<typename T>
void setName(T&& param) // param 是万能引用
{name = std::forward<T>(param);}
}
且在按值返回的函数中,如果返回的是绑定到一个右值引用或者一个万能引用的对象,在返回该引用时,应当对其使用 std::move 或者是 std::forward,这样当可以移动时,会进行移动操作,避免制造副本的成本,提高效率。
Blob operator+(Blob&& lhs, const Blob& rhs){ // 按值返回
lhs += rhs;
return std::move(lhs);
}
template<typename T>
Fraction reduceAndCopy(T&& frac){ // 按值返回
frac.reduce();
return std::forward<T>(frac);
}
但是也不能直接推广到对函数的局部对象使用 std::move 或者是 std::forward,具体用法可以参考一些现有工程源码来参照总结学习。
2460

被折叠的 条评论
为什么被折叠?



