跳转至

Move语义

在 C++11 以前,只能使用一种方式来传递对象——拷贝构造(赋值)。为了安全,若对象同时拥有栈和堆上数据时,我们会手动实现这个函数,将堆上数据再拷贝一份,这种深拷贝在多次赋值时拖慢了效率。

C++11 后,为了实现只拷贝栈上数据,不拷贝堆上数据(浅拷贝,又称移动)的语义,在原先左右值的基础上进行了扩展,引入了将亡值(xvalue)。于是现在有了三种值类型——左值(lvalue)纯右字面值(prvalue)将亡值(xvalue)。

先不考虑 C++ 为啥引入这个,想想该如何设计浅拷贝这个需求?理想状态下,应该是调用一个类似 a.move_to(_T& b) 的方法,使 a 让出堆上数据的所有权(指针地址)给 b,然后自动析构,再也不使用 a。这样似乎更符合 “谁的数据谁做主” 的思想,但是由于拷贝构造函数使用了类似 b.copy_from(const _T& a) 的语法,于是为了统一,这便是 C++ 给出的答案:b.move_from(_T&& a),在 b 的该函数中转移指针,并析构掉在栈上的 a

这就有点抢夺别人的数据的意思,但是意义上是等价的。那么为了和拷贝区别开,C++ 特化了这种构造(赋值)函数,其参数只能接受右值,优先级别比拷贝高,也就是说”能移动就移动“。

那么,如果是函数表达式返回值这一类不具名的值,它显然是右值,直接移动就好,显然提升了效率。但是如果已经有了一个对象 a,需要浅拷贝到某个容器中,并不再使用 a非常不建议保留原来的指针并使用,除非你知道自己在做什么),直接 _T b(a) 显然是不行的,因为 a 是左值,这样只会调用拷贝构造。

于是 std::move(a) 闪亮登场。其作用仅仅是类型转换,将左值变量 a 转换成右值(xvalue)。请注意这个函数对 a 内部没有任何影响。相当于是给 a 打了 tag 表示 a 现在是右值,可以移动了,但并不会进一步操作。操作是接受右值的函数内部进行的。

总结:

  • xvalue可以被移动构造函数直接接受;
  • 移动构造lvalue,那么需要先使用 std::move 将lvalue变成xvalue,从而不调用拷贝,而调用移动;
  • prvalue被接受的时候,生成了一个临时变量xvalue,函数内操作是对这个xvalue进行(这个无所谓,重要的是上面)

references

什么是move?理解C++ Value categories,move, move in Rust - 知乎