C++11新标准中的一个最主要的特性就是移动而非拷贝对象的能力。接下来简要介绍一下相关概念。
-
右值引用
-
所谓右值引用就是必须绑定到右值的引用。通过 && 而不是 & 来获得右值引用。右值引用有一个重要的性质 — 只能绑定到一个将要销毁的对象。因此我们可以自由的将一个右值引用的资源“移动”到另一个对象中。
-
一般而言,一个左值表达式表示的是一个对象的身份,而一个右值表达式表示的是对象的值。
-
类似任何引用,一个右值引用也不过是某个对象的一个名字而已。对于一个常规引用(左值引用),不能将其绑定到要求转换的表达式、字面常量或是返回右值的表达式。右值引用则相反:可以将一个右值引用绑定到这类表达式上,但不能将一个右值引用直接绑定到一个左值上。
int i = 42; int &r = i; //正确, r 引用 i, 左值引用 int &&r = i; //错误,不能将一个右值引用绑定到一个左值上 int &r2 = i * 42; //错误, i * 42 是一个右值 const int &r3 = i * 42; //正确,可以将一个const的引用绑定到一个右值上 int &&rr2 = i * 42; //正确,将rr2绑定到乘法结果上
1. 返回左值引用的函数,连同赋值、下标、解引用和前置递增、递减运算符,都是返回左值的表达式的例子。 2. 返回非引用类型的函数,连同算术、关系、位以及后置递增、递减运算符,都生成右值。我们不能将一个左值引用绑定到这类表达式上,但我们可以将一个const的左值引用和一个右值引用绑定到这类表达式上。
-
左值持久,右值短暂
- 左值有持久的状态,而右值要么是字面常量,要么是在表达式求值过程中创建的临时对象。
- 右值引用指向将要被销毁的对象。因此,可以从绑定到右值引用的对象“窃取”状态,即使用右值引用的代码可以自由接管所引用的对象的资源。
-
变量是左值,不能将一个右值引用直接绑定到一个变量上,即使这个变量是右值引用类型也不行。
int &&rr1 = 42; //正确,字面常量是右值 int &&rr2 = rr1; //错误,表达式rr1是左值
-
标准库的 move 函数
- 虽然不能将一个右值直接绑定到一个左值上,但可以显式地将一个左值转换为对应的右值引用类型。我们可以通过调用一个名为 move的新标准库函数来获得绑定到左值上的引用。头文件
int &&rr3 = std::move(rr1); //ok
- 我们可以销毁一个移后源对象,也可以赋予它新值,但不能使用一个移后源对象的值。
- 虽然不能将一个右值直接绑定到一个左值上,但可以显式地将一个左值转换为对应的右值引用类型。我们可以通过调用一个名为 move的新标准库函数来获得绑定到左值上的引用。头文件
-
-
移动构造函数和移动赋值运算符
- 类似拷贝构造函数,移动构造函数的第一个参数是该类类型的一个引用。不同于拷贝构造函数的是,这个引用参数在移动构造函数中是一个右值引用。与拷贝构造函数相同,任何额外的参数都必须有默认实参。
- 除了完成资源移动,移动构造函数还必须确保移后源对象处于这样一个状态 — 销毁它是无害的。特别是,一旦资源完成移动,源对象必须不再指向被移动的资源。这些资源的所有权已经归属于新创建的对象。
- 移动操作、标准库容器和异常
- 由于移动操作“窃取”资源,它通常不会分配任何资源。因此,移动操作通常不会抛出任何异常。
- 当编写一个不抛出异常的移动操作时,我们应该将此事通知标准库。除非标准库知道不会抛出异常,否则它会为了处理可能抛出异常这种可能性而做一些额外的工作。
- 一种通知标准库的方法是将构造函数指明为 noexcept。这个关键字是新标准引入的。
- 不抛出异常的移动构造函数和移动赋值运算符都必须标记为noexcept.
-
移后源对象必须可析构
- 从一个对象移动数据并不会销毁此对象,但有时在移动操作完成后,源对象会被销毁。当我们编写一个移动操作时,必须确保移后源对象进入一个可析构的状态。即将移后源对象的指针成员置为nullptr来实现的。
- 在移动操作之后,移后源对象必须保持有效的、可析构的状态,但是用户不能对其值进行任何假设。
-
合成的移动操作
- 如果一个类定义了自己的拷贝构造函数、拷贝赋值运算符或者析构函数,编译器就不会为它合成移动构造函数和移动赋值运算符了。
- 只有当一个类没有定义任何自己版本的拷贝控制成员,且类的每个非static数据都可以移动时,编译器才会为它合成移动构造函数和移动赋值运算符。
- 定义了一个移动构造函数或移动赋值运算符的类必须也定义自己的拷贝操作。否则,这些成员默认被定义为删除的。