理解std::move和std::forward
std::movec++11中提供了std::move()来将左值转换为右值引用,从而方便的使用移动语义。move是将对象的状态或者所有权从一个对象转移到另一个对象,只是转移,没有内存拷贝。c++中所有容器都实现了move语义,方便我们实现性能优化。move对于拥有形如对内存、文件句柄等资源的成员的对象有效。如果是一些基本类型,比如int或char[10]数组等,如果使用m...
std::move
c++11中提供了std::move()来将左值转换为右值引用,从而方便的使用移动语义。move是将对象的状态或者所有权从一个对象转移到另一个对象,只是转移,没有内存拷贝。
c++中所有容器都实现了move语义,方便我们实现性能优化。move对于拥有形如对内存、文件句柄等资源的成员的对象有效。如果是一些基本类型,比如int或char[10]数组等,如果使用move,仍然会发生拷贝(因为没有对应的移动构造函数)。
std::list<std::string> tokens;
//发生了移动构造。list的实现,将目的资源句柄赋值为源资源句柄,而将源资源句柄清空
std::list<std::string> t = std::move(tokens);
std::forward
右值引用类型是独立于值的,一个右值引用参数作为函数的形参,在函数内部再转发该参数的时候它已经变成一个左值,并不是他原来的类型。
需要一种方法能够按照参数原来的类型转发到另一个函数,这种转发类型称为完美转发。
完美转发(Perfect Forwarding),是指在函数模板中,完全依照模板的参数的类型(即保持参数的左值、右值特征),将参数传递给函数模板中调用的另外一个函数。C++11中提供了这样的一个函数std::forward,它是为转发而生的,不管参数是T&&这种未定的引用还是明确的左值引用或者右值引用,它会按照参数本来的类型转发。
#include<iostream>
using namespace std;
template<typename T>
void print(T& t)
{
cout << "lvalue" << endl;
}
template<typename T>
void print(T&& t)
{
cout << "rvalue" << endl;
}
template<typename T>
void TestForward(T && v)
{
//print(v); //编译错误 不知道是哪个print
print(std::forward<T>(v));
print(std::move(v));
}
int main()
{
TestForward(1);
int x = 1;
//TestForward(x); //使print(std::forward<T>(v));编译错误
TestForward(std::forward<int>(x));
return 0;
}
//rvalue
//rvalue
//rvalue
//rvalue
std::move没有move任何东西,std::forward没有转发任何东西。在运行期,它们没有做任何事情。它们没有产生需要执行的代码,一byte都没有。
std::move和std::forward只不过就是执行cast的两个函数(实际上是函数模板)。std::move无条件地把它的参数转换成一个右值,而std::forward只在特定条件满足的情况下执行这个转换。
这里给出C++11中std::move实现的一个例子。它没有完全遵循标准的细节,但是很接近了。
template<typename T> //在命名空间std中
typename remove_reference<T>::type&& move(T&& param)
{
using ReturnType = typename remove_reference<T>::type&&; //别名声明
return static_cast<ReturnType>(param);
}
std::move所做的所有事情就是转换它的参数为一个右值。它做的是转换,没有做move。
std::forward的情况和std::move相类似,但是std::forward是一个有条件的转换。为了理解它什么时候转换,什么时候不转换,回忆一下std::forward是怎么使用的。最常见的情况就是,一个带universal引用的参数被传给另外一个参数:
void process(const Widget& lvalArg); // 参数为左值
void process(Widget&& rvalArg); // 参数为右值
template<typename T> // 把参数传给process
void logAndProcess(T&& param) // 的模板
{
process(std::forward<T>(param));
}
考虑一下两个logAndProcess调用,一个使用左值,另外一个使用右值:
Widget w;
logAndProcess(w); // 用左值调用
logAndProcess(std::move(w)); // 用右值调用
当我们用左值调用logAndProcess的时候,我们自然是希望这个左值作为一个左值被转发给process,然后当我们使用右值调用logAndProcess时,我们希望右值版本的process被调用。
但是param就和所有的函数参数一样,是一个左值。因此在logAndProcess内部总是调用左值版本的process。为了防止这样的事情发生,我们需要一种机制来让param在它被一个右值初始化(传给logAndProcess的参数)的时候转换成右值。这正好就是std::forward做的事情。这也就是为什么std::forward是一个条件转换:它只把用右值初始化的参数转换成右值。
我们是不是可以去掉std::move并且在所有的地方都只使用std::forward。从技术的角度来看,回答是可以:std::forward能做到所有的事情。std::move不是必须的。当然,这两个函数函数都不是“必须的”,因为我们能在使用的地方写cast,但是我希望我们能同意它们是必须的函数。
std::move的优点是方便,减少相似的错误,并且更加清晰。考虑一个类,对于这个类我们想要记录它的move构造函数被调用了多少次。
class Widget {
public:
Widget(Widget&& rhs)
: s(std::move(rhs.s))
{ ++moveCtorCalls;}
}
...
private:
static std::size_t moveCtorCalls;
std::string s;
};
为了用std::forward来实现相同的行为,代码看起来像是这样的:
class Widget {
public:
Widget(Wdiget&& rhs) //不常见,以及不受欢迎的实现
: s(std::forward<std::string>(rhs.s))
//译注:为什么是std::string请看Item 1,用右值传入std::string&& str的话
//推导的结果T就是std::string,用左值传入,则推导的结果T会是std::string&
//然后这个T就需要拿来用作forward的模板类型参数了。
//详细的解释可以参考Item28
{ ++moveCtorCalls; }
};
首先注意std::move只需要一个函数参数(rhs.s),而std::forward却需要一个函数参数(rhs.s)以及一个模板类型参数(std::string)。然后注意一下我们传给std::forward的类型应该是一个非引用类型,因为我们约定好传入右值的时候要这么编码(传入一个非引用类型,看Item 28)。也就是说,这意味着std::move需要输入的东西比std::forward更少,还有,它去掉了我们传入的参数是右值时的麻烦(记住类型参数的编码)。它也消除了我们传入错误类型(比如,std::string&,这会导致数据成员用拷贝构造函数来替换move构造函数)的可能。
更加重要的是,使用std::move表示无条件转换到一个右值,然后使用std::forward表示只有引用的是右值时才转换到右值。这是两种非常不同的行为。第一个常常执行move操作,但是第二个只是传递(转发)一个对象给另外一个函数并且保留它原始的左值属性或右值属性。因为这些行为如此地不同,所以我们使用两个函数(以及函数名)来区分它们是很好的主意。
你要记住的事
- std::move无条件转换到右值。就其本身而言,它没有move任何东西。
- std::forward只有在它的参数绑定到一个右值上的时候,它才转换它的参数到一个右值。
- std::move和std::forward在运行期都没有做任何事情。
更多推荐
所有评论(0)