很多人以为 C++ 中的 std::move() 或者所谓的 move semantics (很多人认为这两者等同)就是对内存中的对象进行移动。更有甚者,认为只要命令内存,把一块内存移动到另一个地方,这块内存就像一个乐高一样,被拆了,然后装到另一个地方上去。

内存数据的移动和复制

其实内存没有这么厉害的功能啦!当我们讲“将一块内存移动到另一个地方”的时候,意思是指将这块内存中的数据“移动”到另一个地方。由于内存真的无法进行“移动(物理)”,所以实际上这个过程通常包含两步:

  1. 在要移动到的地方,将来源内存中的数据复制过来

  2. (可选) 删除或破坏来源数据

由于第2步通常是可有可无的,所以通常都不会被实现。于是,一个内存“移动”的操作,往往只是一个内存“复制”操作。差别在于,在内存复制了以后,你是否从心理上还需要来源内存中的数据为原始数据。

其实,不管是“复制”还是“移动”内存中的数据,实现上通常是没区别的。这在对于来源内存和目标内存有交叉的情况中,比较容易见到。这种情况下,当我们需要实现一个内存复制的时候,往往有两种策略:

  1. 报错。内存有交叉,复制必定破坏其中一方的数据

  2. 覆盖来源内存中的数据

现实中,基础库里的内存复制函数,通常都会采用第2种策略。在这种策略里,来源内存中的数据不再被重视,活脱脱的把“复制”内存做成一个“移动”内存。

看到这里,您是否已经发现,“移动”内存和“复制”内存,其实并没有什么差别?

当然,以上这些,都和 C++ 中的所谓 move semantics 无关。

C++ 中并不存在 move semantics

是的,你没看错, C++ 中真的没有 move semantics 的概念。翻开 ISO/IEC 14882:2011[1],也就是 ISO C++ 标准文档,里面你是找不到 move semantics 这个概念的。在正式进入标准以前,需要对标准将要进行的修改进行提案,这个时期就把一些相关的提案通俗的称为 move semantics,于是民间就有了 move semantics 的流传。

std::move() 与 value categories

在大部分人眼里,move semantics 等同于 std::move() 函数,std::move() 等同于对象(的内存)的“移动”,与“复制”相对立。然而,并非如此。

C++ 中其实有两套类型系统。一套就是我们熟知的“类型系统”,我们熟知的各种类型如 int, bool 等等都属于这套类型系统。另一套类型系统,叫 value category 。这两套类型系统是互相独立的。举个例子,表达式42 的类型是 int ,value category 是 prvalue,int 和 prvalue 是表达式 42 在两个不同的类型系统中的 类型

一共有如下几个 value categories 。

stateDiagram-v2
    vc : Value Categories
    vc --> glvalue
    vc --> rvalue
    glvalue --> lvalue
    glvalue --> xvalue
    rvalue --> xvalue
    rvalue --> prvalue

和“类型系统”一样,属于哪种 value category ,是可以通过表达式和上下文推导出来的。既然 value categories 也是一种类型系统,那么也就可以进行“类型转换”了。std::move() 的功能,就是提供一个表达式,这个表达式的 value category 是 xvalue 。例如,42 这个表达式的 value category 是 prvalue,std::move(42) 的 value category 是 xvalue ,相当于把 42 这个表达式的 value category 进行了一次转换。

move constructor 以及 move assignment operator

除了 std::move(),一般提及 move semantics 还会提及 move constructor 以及 move assignment operator 这两个回调函数。一般的教材会跟你讲,这两个会在“移动”的时候触发,目的是让你手动的把资源从原来的对象“移动”到当前对象。然而,并非如此。“移动”这个概念本身是不存在的,最多只是调用一下 std::move() ,但这仅仅只一个普通的函数调用,并无任何特别之处。而 move constructor 和 move assignment operator 并无所谓的“移动”的语义,它的内容,可以是任意的。并且,你不可以依赖它来“移动”。那么,move constructor 和 move assignment operator 的作用和语义是什么呢?

move constructor 是普通的 constructor 的一种,就像一个普通的 constructor 一样被调用。当你用于初始化一个对象的参数,是同类型的 rvalue 的时候,这个 constructor 会被调用。举个例子,a, b 都是 T 类型的对象,那么,T a(std::move(b)); 在创建对象 a 的时候就会调用 move constructor,因为传入的参数 std::move(b) 是一个 xvalue,而 xvalue 本身也是 rvalue 。在调用完以后,对象 b 还在,且内容只要不被手动破坏,都是正常的。[2]

move assignment operator 也是类似,它只是一个普通的 assignment operator 。当赋值的时候,如果 move assignment operator 被选中,那么就会执行。例如下面的代码:

1
2
3
4
5
void foo()
{
T a, b;
a = std::move(b);
}

在这个代码中,赋值 a = std::move(b); 中,a 是 lvalue,std::move(b) 是 xvalue ,也是 rvalue ,因此 move assignment operator 被选中,然后调用。同样的,只要不手动破坏,b 的内容都是正常的。

无论是 move constructor 还是 move assignment operator ,其实都只是一个普通的 constructor 或 operator overloading 。之所以触发它们的调用,仅仅只是因为它们的原型,让其在 overload resolution 的过程中被选中而已。

其实还有一点要注意,std::move() 本身并不会触发 move constructor 和 move assignment constructor 这两个回调函数。std::move() 只是默默的提供一个 xvalue 而已,本身什么也不做。而后续 move constructor 和 move assignment constructor 被触发调用,仅仅也只是因为参数 xvalue 让其在后续的 overload resolution 中被选中。


  1. move semantics 的提案从这个版本开始加入并一直存在至今。 ↩︎

  2. 通常教科书会说必须在创建新对象的同时破坏旧的对象。可能目的是想让看起来更像是“移动”吧。 ↩︎