Non-Profit, International

Spirit unsterblich.

C++ std::move 和右值引用的使用

字数统计:979 blog

右值是一个被说烂了的问题,之前的文章多次讲过右值和右值引用的语法,如何编写移动构造移动赋值等,对于如何真正应用却缺乏描述,这篇文章就是用来说明如何利用移动。

实际上整个右值和移动体系,最核心的场景只有一个:


void f(A);

int main() {
    A a;
    f(std::move(a));
}

稍微了解历史的人会知道 auto_ptr 之殇。之前我的文章里多次提到过,移动操作在实现上其实是交换,并不是什么神奇的魔术,auto_ptr 用复制和交换其实能够实现现在所谓的移动的大部分操作,并不需要理解 C++11 引入的这些晦涩的概念。但是,auto_ptr 不是没有问题,auto_ptr 的问题正如后人所记载的:1. 按成员复制来做到传递资源是有缺陷的,这种方式可以类比于连接,而真正需要的是转移。2. 与容器不兼容。

那么,解决方案就是上述代码:f 函数内部持有自己的 A 对象;外部的 A 对象能传递给 f。这才是真正做到了资源的转移,这才是移动的真实目的。

移动赋值其实就相对不那么重要,因为移动赋值其实就是 swap。并且右值引用作为函数参数时,其实也是引用而不是对象,也不那么重要。右值引用作为函数参数的唯一目的是强迫你去 move

注意,对象作为参数和右值引用作为参数,都能接收 move 后的对象,但是右值引用是利用了原有对象,并没有发生移动构造。而对象作为参数的时候是存在新的对象,而且使用了移动构造。实际上我愿称前一种方式为传递对象(资源),后一种方式为传递所有权。

右值引用作为参数显然是很鸡肋的,因为它既不能保证资源真转移走了,又造成这种假象,实际上只是一个普通引用而已。

右值引用被发明出来的核心目的是为了 消除容器添加元素时的歧义 。最简单的例子是 std::vector<T, A>::push_back

试想一下现在待插入的元素有两种情况:左值和右值,对于左值,理所当然的使用 void push_back(T& a),对于右值,使用 void push_back(T a) 貌似也天经地义,但是麻烦的地方在于,void push_back(T a) 这个函数不仅仅能接受右值的实参,也能够接受左值的实参,换句话说,如果存在一个左值被等待插入,那么有歧义。所以必须要设计出一种不干扰左值引用的形参,那就是右值引用。

对象作为参数的缺点很明显,对象是占用空间的,vector 等类占用空间大概 3 个指针,并且会有析构函数的负担。但优点更明显:函数内的对象是属于函数自己的,只活在自己的作用域。以往写代码的时候我曾经误用过 move 后的对象,该对象被传递到一个右值引用为参数的函数中。这在单线程中是没太大问题的,但是多线程中由于右值引用为参数获得的是引用,所以 move 后就真的不能再利用了,否则会发生竞争。但是如果参数是对象,那还可以复用被 move 的对象。

如果不是为了消歧,我建议需要移动的情况还是使用对象形参而不是引用形参,尤其是 std::unique_ptrstd::shared_ptr

上述那个简短的代码片段就是 std::unique_ptrstd::shared_ptr 真正强于 auto_ptr 的所在之处。对于非模板容器代码,我认为应该避免使用右值引用作为函数参数,剩下的使用方式读者就仔细体会吧。


若无特殊声明,本人原创文章以 CC BY-SA 3.0 许可协议 提供。