Effective Modern C++ Item 23 的学习和解读。
std::move 和 std::forward 并不像他们名字所表达的那样,实际上 std::move 并没有移动数据,std::forward 也并没有转发数据,并且它们在运行期什么也没做。
先说 std::move,我们看下它在 C++11 中简易的实现如下:
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 只是返回了右值引用。这里使用了 remove_reference 是为了去除引用标识符。当 T 是一个引用类型的时候,根据引用折叠原理,T&& 会被折叠成一个左值引用类型。所以 remove_reference 是为了去防止 T 是一个引用类型, 它会去除引用进而保证 std::move 返回一个右值引用。因此 std::move 只是做了类型转换,并没有移动数据。由于只有右值是可以被移动的,std::move 更像是说明经过它之后对象可能会被移动(可能,而不是一定,后文会有解释)。
而 C++14 的 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 的目的就是让编译器把修饰的变量看做是右值,进而就可以调用其移动构造函数。事实上,右值是仅可以被移动的对象,std::move 之后不一定一定调用构造函数。看下面的例子,假如你有这样的一个类:
class Annotation {
public:
explicit Annotation(std::string text) : text_(text)
std::string text_;
}
class Annotation {
public:
explicit Annotation(std::string text) : text_(std::move(text)) {}
std::string text_;
};
class Annotation {
public:
//这里换成了带有const
explicit Annotation(const std::string text) : text_(std::move(text)) {}
std::string text_;
};
第一个实现会发生两次拷贝,第二个实现会发生一次拷贝和一次移动,那么第三个实现会发生什么呢?
由于 Annotation 的构造函数传入的是一个 const std::string text,std::move(text) 会返回一个常量右值引用,也就是 const 属性被保留了下来。而 std::string 的 move 构造函数的参数只能是一个非 const 的右值引用,这里不能去调用 move 构造。只能调用 copy 构造,因为 copy 构造函数的参数是一个 const 引用,它是可以指向一个 const 右值。因此,第三个实现也是发生两次拷贝。
也可以用下面的例子验证一下:
#include
#include
using boost::typeindex::type_id_with_cvr;
class A {
public:
A(){
std::cout << "constructon" << std::endl;
}
A(const A& a) {
std::cout << "copy constructon" << std::endl;
}
A(A&& a) {
std::cout << "move constructon" << std::endl;
}
};
int main() {
const A a1;
std::cout << type_id_with_cvr<decltype(std::move(a1))>().pretty_name() << std::endl;
auto a2(std::move(a1));
return 0;
}
// output
constructon
A const&&
copy constructon
因此,我们可以总结出两点启示:
再说 std::forward。std::forward 也并没有转发数据,本质上只是做类型转换,与 std::move 不同的是,std::move 是将数据无条件的转换右值,而 std::forward 的转换是有条件的:当传入的是右值的时候将其转换为右值类型。
看一个 std::forward 的典型应用:
#include
#include
class Widget {
};
void process(const Widget& lvalArg) {
std::cout << "process(const Widget& lvalArg)" << std::endl;
}
void process(Widget&& rvalArg) {
std::cout << "process(Widget&& rvalArg)" << std::endl;
}
template<typename T>
void logAndProcess(T&& param) {
auto now = std::chrono::system_clock::now();
process(std::forward<T>(param));
}
int main () {
Widget w;
logAndProcess(w); // call with lvalue
logAndProcess(std::move(w)); // call with rvalue
}
// output
process(const Widget& lvalArg)
process(Widget&& rvalArg)
当我们通过左值去调用 logAndProcess 时,自然期望这个左值可以同样作为一个左值转移到 process 函数,当我们通过右值去调用 logAndProcess 时,我们期望这个右值可以同样作为一个右值转移到 process 函数。
但是,对于 logAndProcess 的参数 param,它是个左值(可以取地址)。在 logAndProcess 内部只会调用左值的 process 函数。为了避免这个问题,当且仅当传入的用来初始化 param 的实参是个右值,我们需要 std::forward 来把 param 转换成一个右值。至于 std::forward 是如何知道它的参数是通过一个右值来初始化的,将会在 Item 28 中会解释这个问题。
总结一下: