C++ 不会 “move”
很多人以为 C++ 中的 std::move()
或者所谓的 move semantics (很多人认为这两者等同)就是对内存中的对象进行移动。更有甚者,认为只要命令内存,把一块内存移动到另一个地方,这块内存就像一个乐高一样,被拆了,然后装到另一个地方上去。
内存数据的移动和复制
其实内存没有这么厉害的功能啦!当我们讲“将一块内存移动到另一个地方”的时候,意思是指将这块内存中的数据“移动”到另一个地方。由于内存真的无法进行“移动(物理)”,所以实际上这个过程通常包含两步:
-
在要移动到的地方,将来源内存中的数据复制过来
-
(可选) 删除或破坏来源数据
由于第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 | void foo() |
在这个代码中,赋值 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 中被选中。