通过了解std::move和std::forward不做什么来理解它们很有用。std::move不移动任何东西,std::forward也不转移任何东西。在运行时(runtime),他们什么都不做,一行代码也不产生。
std::move和std::forward仅仅是进行类型转换的函数(实际上是函数模板)。std::move无条件的将其参数转换为右值,而std::forward只在必要情况下进行这个转换,就是这样。这个解释会引起一系列问题,但是基本上这就是完整的故事。
为了让故事更加具体,这有一个在c++11中std::move的模拟实现:
template<typename T> // in namespace std
typename remove_reference<T>::type&&
move(T&& param)
{
using ReturnType = // alias declaration;
typename remove_reference<T>::type&&; // see Item 9
return static_cast<ReturnType>(param);
}
我高亮了两处代码。一个是函数的名字,因为返回值很繁琐,我怕你失去忍耐。另一处是函数的精华本质部分:转换。如你所见,std::move的参数为一个对象的引用(统一引用,详见条款24),并且返回的也是该对象的引用。
函数返回值的“&&”部分表示std::move返回了一个右值引用,但是如条款28所述,假如类型T碰巧是个左值引用,T&&就会成为一个左值引用。为阻止这个发生,一个type trait(见条款9)std::remove_reference被用在T上,这样可以确保“&&”可以应用在一个不是引用的类型上,这个很重要,因为函数返回的右值引用必须是右值。于是,std::move将其参数转换右值,这就是它所有做的事情。
此外,std::move在c++14中的实现显得更简短。由于函数返回类型推导(条款3)和标准库里的别名模板std::remove_reference_t (见条款9),std::move可以这样实现:
template<typename T> // C++14; still in
decltype(auto) move(T&& param) // namespace std
{
using ReturnType = remove_reference_t<T>&&;
return static_cast<ReturnType>(param);
}
看上去更简单些,不是?
因为std::move除了把参数转换为右值以外不做别的事情,有建议给它一个更好的名字也许类似rvalue_cast,即使如此,我们现在拥有的名字是std::move,所以认识到std::move做什么和不做什么很重要。它做的是转换,不做移动。
当然,右值适合被移动,所以std::move应用在一个对象上时就是告诉编译器对象可以被移动。这就是为什么std::move拥有这样的名字:使得指定可以被移动的对象更容易些。
事实上,右值是仅有的可以被移动的对象。假如你写了个类代表注释(annotation),类的构造函数使用std::string类型做为参数表示注释内容,并且拷贝参数到一个成员变量里。根据条款41的信息,你声明了一个传值参数:
class Annotation {
public:
explicit Annotation(std::string text); // param to be copied,
… // so per Item 41,
}; // pass by value
但是Annotation的构造函数仅仅需要读取text的值,不需要改变它。根据我们固有的传统,尽可能的使用const,你改变了声明如下:
class Annotation {
public:
explicit Annotation(const std::string text)
…
};
为了避免当拷贝text到数据成员中时消耗一次拷贝操作,你使用了条款41的建议,将std::move应用到text上,于是产生了一个右值:
class Annotation {
public:
explicit Annotation(const std::string text)
: value(std::move(text)) // "move" text into value; this code
{ … } // doesn't do what it seems to!
…
private:
std::string value;
};
代码编译链接都可以正常,也把设置了数据成员为text的内容。唯一使得这段代码和你想象中的完美实现不一样的地方是text不是移动到value的,是拷贝的。当然,text是通过std::move转换成一个右值,但是text是被声明成const std::string,所以在转换前,text是一个左值的const std::string,转换的结果是一个右值const std::string,最终常量性保留了下来。
考虑下当编译器必须决定哪个std::string构造函数必须调用时的效果,有两个可能:
class string { // std::string is actually a
public: // typedef for std::basic_string<char>
…
string(const string& rhs); // copy ctor 拷贝构造函数
string(string&& rhs); // move ctor move移动构造函数
…
};
在Annotation的构造函数的成员初始化列表里,std::move(text)的结果是一个类型为const std::string的右值。这个右值不能传递给std::string的move构造函数,因为move构造函数需要一个指向非常量std::string的右值引用作为参数。然而这个右值可以传递给拷贝构造函数,因为指向常量的左值引用是允许绑定到一个常量右值的。于是成员初始化就会调用std::string的拷贝构造函数,即使text已经转换成一个右值。这样的行为是维持常量正确性的一个必需,因为移动一个对象到另一个值通常会改变这个对象,所以语言就不应该允许常量对象被传递给一个能修改他们的函数(比如move构造)。
从这个例子可以学到两个教训。第一,假如你想对象能够被移动,不要声明对象为const,在const对象上的移动操作默默的被转换成了拷贝操作。第二,std::move不仅不移动任何东西,甚至不能保证被转换的对象可以被移动。唯一可以确认的是应用std::move的对象结果是个右值。
std::forward的故事和std::move的故事很类似,但std::move是无条件的转换其参数为一个右值,而std::forward是在某些特定条件下进行转换。std::forward是一个有条件转换。为了理解什么时候转换什么时候不转换,回忆一下std::forward是怎么使用的。最常见的场景是在函数模板,参数是一个统一引用参数,这个参数会被传递给另一个函数。
void process(const Widget& lvalArg); // process lvalues
void process(Widget&& rvalArg); // process rvalues
template<typename T> // template that passes
void logAndProcess(T&& param) // param to process
{
auto now = // get current time
std::chrono::system_clock::now();
makeLogEntry("Calling 'process'", now);
process(std::forward<T>(param));
}
考虑两种调用logAndProcess的情形,一种是左值,一种是右值:
Widget w;
logAndProcess(w); // call with lvalue
logAndProcess(std::move(w)); // call with rvalue
在logAndProcess内部,参数param被传递给函数process,process被重载为左值和右值。当我们通过左值去调用logAndProcess时,我们很自然的期望那个左值可以同样作为一个左值转移到process函数,当我们通过右值去调用logAndProcess时,我们期望重载的右值函数可以被调用。
但是param,像所有函数参数一样,是个左值。在logAndProcess内部每一个对process的调用会调起左值重载。为了避免这个,我们需要一个机制来把param转换成一个右值,当且仅当传入的用来初始化parm的实参(就是传递到logAndProcess的参数)是个右值。
你可能疑惑std::forward是怎么知道它的参数是通过一个右值来初始化的。比如在以上代码中,std::forward是怎么知道param是被一个左值或右值来初始化呢?简单的回答是这个信息被编码到logAndProcess的模板参数T里面。这个参数被传递给std::forward,然后恢复了编码的信息。详细的描述见条款28。
既然std::move和std::forward都归结为转换,唯一不同就是std::move始终进行转换,而std::forward仅仅有时候转换,你可能会问我们是否可以废弃std::move,而只用std::forward。从纯技术角度来看,答案是肯定的:std::forward可以做到,std::move不是必须的。当然函数也不是必须的,我们可以写转换代码,但是我希望我们会觉得那样比较恶心。
std::move吸引人之处在于方便,减少了错误的可能性,而且更加清晰。考虑一个类,可以跟踪我们使用了多少次的move构造函数。我们所需要的是一个静态变量的计数器,在move构造函数使用时增加。假设类里面的唯一一个非静态数据成员是std::string,这里有个方便的方法(也就是利用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(Widget&& rhs) // unconventional,
: s(std::forward<std::string>(rhs.s)) // undesirable
{ ++moveCtorCalls; } // implementation
…
};
注意首先std::move只需要一个函数参数(rhs.s),而std::forward既需要一个函数参数(rhs.s),又需要一个模板类型参数(std::string)。然后我们注意到我们传递给std::forward的参数类型必须一个非引用的,因为编码规范上说明了传递的参数必须是一个右值(见条款28)。std::move需要更少的打字,and it spares us the trouble of passing a type argument。也减少了我们传递一个错误类型(比如std::string&,这会导致数据成员s被拷贝构造而不是move构造)的可能性。
更重要的是,使用std::move会无条件转换为一个右值,而使用std::forward只会把是绑定到右值引用的参数转换成右值。这是两个非常不同的行为。第一个是典型的move,而第二个是仅仅传递(转移)一个对象到另一个函数,通过这种办法来保留对象原始的左值性或右值性。因为两者如此不同,所以最好我们使用不同的函数(名)来区分它们。
要记住的事情
1.std::move执行一个无条件的转化到右值。它本身并不移动任何东西;
2.std::forward把其参数转换为右值,仅仅在那个参数被绑定到一个右值时;
3.std::move和std::forward在运行时(runtime)都不做任何事。