为了支持移动操作,新标准引入了一种新的引用类型——右值引用,所谓右值引用就是必须绑定右值的引用,我们通过&&而不是&来获得右值引用。右值引用有一个重要的性质:只能绑定到一个将要销毁的对象。因此我们可以自由地将一个右值引用的资源“移动”到另一个对象中。
一个左值表达式表示的是一个对象的身份,而一个右值表达式表示的是对象的值。
类似任何引用,一个右值引用也不过是某个对象的另一个名字而已。对于左值引用,我们不能将其绑定到要求转换的表达式、字面常量或是返回右值的表达式。右值引用有着完全相反的绑定特性:我们可以将一个右值引用绑定到这类表达式上,但不能将一个右值引用直接绑定到一个左值上:
int i = 42; int &r = i; //正确,r引用i int &&rr = i; //错误,不能将一个右值引用绑定到一个左值上 int &r2 = i * 42; // //错误,i*42是一个右值 const int &r3 = i * 42; //正确,我们可以将一个 const 引用绑定到一个右值上 int &&rr2 = i * 42; //正确,将rr2绑定到乘法结果上
返回左值引用的函数,连同赋值、下标、解引用和前置递增/递减运算符,都是返回左值的表达式的例子。我们可以将一个左值引用绑定到这类表达式的结果上。
返回非引用类型的函数,连同算数、关系、位以及后置递增/递减运算符,都生成右值。我们不能将一个左值引用绑定到这类表达式上,当我们可以将一个const的左值引用或者一个右值引用绑定到这类表达式上。
左值有持久的状态,而右值要么是字面常量,要么是在常量表达式求职过程中创建的临时对象。
由于右值引用只能绑定到临时对象,我们得知:
这两个特性意味着:使用右值引用的代码可以自由地接管所引用的对象的资源。
右值引用指向将要被销毁的对象。因此,我们可以从绑定到右值引用的对象“窃取”状态。
int &&rr1 = 42; //正确,字面常量是右值 int &&rr2 = rr1; //错误,表达式rr1是左值
变量是左值,因此我们不能将一个右值引用直接绑定到一个变量上,即使这个变量是右值引用类型也不行。
虽然不能将一个右值引用直接绑定到一个左值上,但我们可以显示的将一个左值转换为对应的右值引用类型。我们还可以通过调用一个名为move的新标准库函数来获得绑定到左值上的右值引用,此函数定义在头文件utility中。
int &&r3 = std::move(rr1); //ok
move调用告诉编译器,我们有一个左值,但我们希望像右值一样处理它。我们必须认识到,调用move就意味着承诺:除了对rr1赋值或销毁它外,我们不能再使用它。在调用move之后,我们不能对移后源对象的值做任何假设.
为了让我们自己的类型支持移动操作,需要为其定义移动构造函数和移动赋值运算符。这两个成员类似对应的拷贝操作,但它们从给定对象“窃取”资源而不是拷贝资源。
类似拷贝构造函数,移动构造函数的第一个参数是该类类型的一个引用,不同于拷贝构造函数的是,这个引用参数在移动构造函数中是一个右值引用。与拷贝构造函数一样,任何额外的参数都必须有默认实参。
除了完成资源移动,移动构造函数还必须确保移后源对象处于这样一个状态——销毁它是无害的。特别是,一旦资源完成移动,源对象必须不再指向被移动的资源——这些资源的所有权已经归属新创建的对象。示例:
StrVec::StrVec(StrVec &&s) noexcept //移动操作不应抛出任何异常 //成员初始化器接管s中的资源 :elements(s.elements), first_free(s.first_free), cap(s.cap) { //另s进入这样一个状态——对其运行析构函数是安全的 s.elements = s.first_free = s.cap = nullptr; }
移动构造函数不分配任何新内存,它接管给定的StrVec中的内存。在接管内存之后,它将给定对象中的指针都置为nullptr。这样就完成了从给定对象的移动操作,此对象将继续存在。最终,移后源对象会被销毁,意味着将在其上运行析构函数,StrVec的析构函数在first_free上调用deallocate。如果我们忘记改变s.first_free,则销毁移动源对象就会释放掉我们刚刚移动的内存。
移动赋值运算符执行与析构函数和移动构造函数相同的工作。与移动构造函数一样,如果我们的移动赋值运算符不抛出任何异常,我们就应该将它标记为noexcept。类似拷贝赋值运算符,移动赋值运算符必须正确处理自赋值:
StrVec& StrVec::operator=(StrVec &&rhs) noexcept { if(this != &rhs) { free(); //释放已有元素 elements = rhs.elements; first_free = rhs.first_free; cap = rhs.cap; //将rhs置于可析构状态 rhs.elements = rhs.first_free = rhs.cap = nullptr; } return *this; }
从一个对象移动数据并不会销毁此对象,但有时在移动操作完成后,源对象会被销毁。因此,当我们编写一个移动操作时,必须确保移后源对象进入一个可析构的状态。
除了将移后源对象置为析构安全的状态之外,移动操作还必须保证对象仍然是有效的。一般来说,对象有效就是指可以安全地为为其赋予新值或者可以安全地使用而不依赖其当前值。另一方面,移动操作对移后源对象中留下的值没有任何要求。因此,我们的程序不应该依赖移后源对象中的数据。
在移动操作之后,移后源对象必须保持有效的、可析构的状态,但是用户不能对其值进行任何假设。
如果一个类定义了自己的拷贝构造函数、拷贝赋值运算或者析构函数,编译器就不会为它合成移动构造函数和移动赋值运算符了。如果一个类没有移动操作,通过正常的函数匹配,类会使用对应的拷贝构造来代替移动操作。
只有当一个类没有定义任何自己版本的拷贝控制成员,且类的每个非static数据成员都可以移动时,编译器才会为它合成移动构造函数或移动赋值运算符。编译器可以移动内置类型的成员。如果一个成员时类类型,且该类有对应的移动操作,编译器也能移动这个成员
//编译器会为X和hasX合成移动操作 struct X{ int x; //内置类型可以移动 std::string s; //string 定义了自己的移动操作 }; struct hasX{ X mem; //X有合成的移动操作 } X x, x2 = std::move(x); //使用合成的移动构造函数 hasX hx, hx2 = std::move(hx); //使用合成的移动构造函数
移动操作永远不会隐式定义为删除的函数。如果我们显示地要求编译器生成=default的移动操作,且编译器不能移动所有成员,则编译器会将移动操作定义为删除的函数。
//假定Y是一个类,它定义了自己的拷贝构造函数但未定义自己的移动构造函数 struct hasY { hasY() = defualt; hasY(hasY&&) = default; Y mem; //hasY将有一个删除的移动构造函数 } hasY hy, hy2 = std::move(hy); //错误,移动构造函数是删除的
编译器可以拷贝类型为Y的对象,但不能移动它们。类hasY显示地要求一个移动构造函数,但编译器无法为其生成。因此,hasY会有一个删除的移动构造函数。如果hasY忽略了移动构造函数的声明,则编译器根本不能为它合成一个。如果移动构造函数被定义为删除的函数,编译器就不会合成它们。
如果类定义了一个移动构造拷贝和/或一个移动赋值运算符,咋该类的合成拷贝构造函数和拷贝赋值运算符将会被定义为删除的。定义了一个移动构造函数或者移动赋值运算符的类必须也定义自己的拷贝操作,否则,这些成员都被默认为删除的。