知道std::move 和std::forward不做哪些事情时,更容易增加对其了解。std::move没有移动任何东西,而std::forward也没有转发任何东西。程序运行的时候,二者都没有做任何事情。二者都不生成运行时代码,一行也没有。
std::move和std::forward只是转换函数(实际模板函数)。std::move无条件 的将参数转成右值,而std::forward也做这件事,只不过要判断一个特殊的条件是否满足。这个解释引入了一系列新问题,基本就是故事的全部了。
让故事更具体一点,这里有个std::move的c++11实现。这个实现和标准并不完全一致,但比较接近。
template <typename T>
typename remove_reference<T>::type&&
move(T && param)
{
using ReturnType =
typename remove_reference<T>::type&&;
return static_cast<ReturnType>(param);
}
我高亮了两个部分。第一是函数名字,这是因为返回值太噪杂了,我不想让你迷失在其中。另外一个是转换,这构成了函数的本质。如你所见,std::move接受一个对象的引用(更精确的说通用引用第24条款),返回了同一个对象的引用。
符号&&表明返回值是一个右值引用,但正如条款28解释的,如果类型T是一个左值引用,那么T&&也是左值引用。适用remove_reference来确保&&作用的类型不是一个引用类型,这也保证了std::move返回右值。这很重要,因为函数返回的右值引用仍然是右值引用。std::move将参数转成右值,这就是它做的全部工作。
在c++14中实现可以更简单一些:
template <typename T>
decltype(auto)
move(T && param)
{
using ReturnType =
tremove_reference_t<T>&&;
return static_cast<ReturnType>(param);
}
看起来清爽许多?
因为std::move没有转移任何东西,所以更应该命名为rvalue_cast.那只是一种可能,目前既然仍然叫move,那么有必要记住它做了哪些,没有做哪些事情。它做了转换,没有做转移。
右值是move候选者。因此std::move告诉编译器对象可以被move。这也是move名称的成因,简单表明对象可以被转移。
右值只是通常的move候选者。设想你写一个注解的类,构造函数接受一个std::string 来构造注解。它将参数拷贝到数据成员中,根据条款41提供的信息,你设计了一个传值的参数:
class Annotation {
public:
explicit Annotaiton(std::string text); // 需要拷贝的参数
};
但Annotation构造函数只需要读取text的值,而不需要改变这个值。根据一个久经考验的传统,你应该尽可能给参数加上const。
class Annotation {
public:
explicit Annotaiton(const std::string text); // 需要拷贝的参数
};
为了避免把text拷贝到成员变量中,你遵照41条款对text做了std::move操作,
class Annotation {
public:
explicit Annotaiton(const std::string text):
:value(std::move(text)) {
}
private:
std::string value;
};
};
代码可以编译,也可以链接,可以运行。这段代码将value的内容设置为text。唯一不是很完美的是text不是move到value的,而是拷贝过去的。尽管text被std::move转换成了右值,但由于text被声明为一个const类型,在转换之前,是一个左值const, 转换后成为一个右值从上const,转换过程不影响const。
考虑编译器会选择调用哪一个std::string构造函数,有两个可能:
class string {
public:
string(const string & rhs); //copy
string(string &&rhs); //move
};
在Annotaiton中,std::move返回一个右值的const 类型string。 这个右值无法传递给string的move构造函数, 因为string的move构造函数接受的是非const的右值。这样即使text已经被转换成了右值,但仍然调用了string的拷贝构造函数。这对保持const语义正确是必须的。
将值从对象中移出去一般算是改变了对象,所以从语言层面就应该禁止将const对象传递给改变它们的函数(例如move)。
从这个例子可以学到2点:第一如果你想move一个对象,就不要将其声明为const类型。对const类型变量的move,会静默的转成拷贝操作。第二,std::move不仅没有移动对象,它甚至不保证转换后的对象是可以move的,它只保证是一个右值。
std::forward的故事与之类似,只不过std::move的无条件将参数转换成右值,而std::forward的转换需要在一定条件下进行。为了弄明白什么时候可以转换,需要回想std::forward的典型应用场景。最典型的应用是将一个模板函数的通用引用参数传递到另外一个函数中。
void process(const Widget &widget); //处理左值
void process(Widget &&widget); //处理右值
template <typename T>
void logAndProcess(T &¶m) {
log(param);
process(std::forward<T>(param));
}
考虑到下面两种对logAndProcess调用,一种是左值,一种是右值:
Widget w
logAndProcess(w);
logAndProcess(std::move(w));
在logAndProcess中,param被传给process。process有重载的左值和右值两个版本。当我们传递给logAndProcess一个左值时,我们希望调用左值版本的process;当传递一个右值时,我们则希望调用process的右值版本。
然而函数参数都是左值的。在logAndProcess内的每一个process调用都是调用左值版本。为了阻止这种情况发生,当传递给param的初始值是右值的时候,就需要将param转换成右值。这也确是std::forward做的事情。这也是为什么std::forward是有条件转换的原因,只有参数被右值初始化的时候,才会转换。
你可能想知道std::forward是怎样知道参数是从右值初始化的。在上面例子中,信息包含在模板参数T中,可以恢复编码信息,具体见28这条款。
std::forward和std::move都可以转换,只不过std::move总是转换,而std::forward有时候转换。你可能问,我们放弃std::move只用std::forward是否可以?从技术上面讲是可以的,std::forward可以完成所有的工作。当然这两个函数都不是完全必须的,因为我们可以在任何地方写手动转换。但是我希望我们能同意这种做法很讨厌。
std::move吸引人之处是方便,出错率小以及清晰。考虑设计一个class用来统计move构造函数的调用次数。一个随着move构造函数递增的count静态变量就是所有所需。假定类中唯一非静态变量是一个string类型,下面是一个传统的(std::move)实现move构造函数的方式:
class Widget {
public:
Widget(Widget && w) :s(std::move(w.s){
++moveCtorCalls;
}
private:
std::string s;
static size_t moveCtorCalls;
};
可以用std::forward实现同样功能:
class Widget {
public:
Widget(Widget && w) :s(std::forward<std::string>(w.s)){
++moveCtorCalls;
}
private:
std::string s;
static size_t moveCtorCalls;
};
第一个用std::move实现的版本只需要一个函数参数(rhs.s),而第二个std::forward实现的版本需要一个函数参数(rhs.s)和模板类型参数(std::string)。值得注意的是传递给std::forward的不应该是一个引用类型。因为通常参数编码成右值。而且std::move需要打更少的子。也阻止了我们传递错误类型(比如传递一个string&,这将会调用拷贝构造函数)。
std::move可以无条件转换成右值,而std::forward只在绑定右值的时候才可以转换。这是两个显著不同的行为。第一个会创造的移动,而第二个则是将对象传递给另外一个函数,同时保持对象原始的左值和右值属性。因为这些行为的不同,我们用了两个名字来对二者区别。
注意事项
std::move 无条件的将对象转成右值类型,本身并不移动任何东西。
std::forward 只是在参数绑定为右值时候,才将其转成右值。
运行时,std::move和std::forward都不做任何操作。