C++ 11标准中有一个很重要的特性:可以移动对象,该特性主要针对这样一种场景:一个对象在被拷贝之后就不在使用了或者马上就会被析构掉,这种情况下,使用移动操作而非拷贝操作将会大幅度提升性能。移动操作的思想是接管源对象的内容。
右值引用
为了支持移动操作,新标准引入一种新的引用类型:右值引用,即绑定到右值的引用,使用&&获取右值引用。右值引用的特点是只能绑定到一个即将销毁的对象,这样就可以把一个右值引用的资源移动到另一个对象中。
左值引用和右值引用的区别
左值引用是对左值的应用,右值是对右值的引用,左值的特点是持久,右值的特点是短暂。返回左值引用的函数,赋值,下标,解引用,前置递增/递减运算符,都是返回左值的表达式,可以将左值引用绑定到这些表达式的结果上。返回非引用类型的函数,算数,关系,位以及后置递增/递减运算符,都是返回右值的表达式,可以将右值引用绑定到这些表达式的结果上。
变量都是左值,不能将右值引用绑定到一个右值引用类型的变量上,下列赋值是错误的:
int &&r1 = 42;
int &&r2 = r1; // 错误
move函数
不能将一个右值引用直接绑定到一个左值上,但是可以显示的将一个左值转化为对应的右值引用类型,通过标准库的move函数实现这一点,该函数定义在头文件中
int &&r3 = std::move(r1); // 正确
调用move之后,除了对r1进行赋值或者销毁之外,不能在使用它,调用move函数之后,不能对移后源对象做任何假设。
移动构造函数和移动赋值运算符
为了使类型支持移动操作,需要为其定义移动构造函数和移动赋值运算符,对应对应的拷贝操作。移动构造函数的第一个参数是该类类型的一个右值引用,和拷贝构造函数一样,任何额外的参数都必须有默认实参。移动构造函数必须确保移后源对象处于这样一个状态:销毁它是无害的。一旦资源完成移动,源对象就不再指向被移动的资源,资源的所有权已经转移了。
class Test
{
public:
Test(Test &&t) noexcept
: s(t.s)
{
t.s = nullptr;
}
// 其它成员
... ...
private:
char *s;
};
移动构造函数不会分配任何新内存,它接管给定的对象的内容,接管之后,源对象中的指针被置为nullptr,如上例中的实例t,在进行移动操作之后,其数据成员s被置为nullptr。
移动赋值运算符和移动构造函数类似:
class Test
{
public:
operator(Test &&rhs) noexcept
{
if (this != &rhs) {
free(s); // 释放已有元素
s = rhs.s; // 从rhs接管资源
rhs.s = nullptr;
}
return *this;
}
// 其它成员
... ...
private:
char *s;
};
合成的移动操作
与处理拷贝构造函数和拷贝赋值运算符一样,编译器也会合成移动构造函数和移动赋值运算符,但是,合成条件和拷贝操作大不相同。
如果我们不声明自己的拷贝构造函数和拷贝赋值运算符,编译器就会为我们合成这样的操作,但是即使我们不声明自己的移动构造函数和移动赋值运算符,编译器在某些类中也不会合成相应的移动操作,因此有些类就不会有移动构造函数或者移动赋值运算符。
只有当一个类没有定义任何自己版本的拷贝控制成员,且类的每个非static数据成员都可以移动时,编译器才会为它合成移动构造函数或者移动赋值运算符:
struct X {
int i;
std::string s;
};
struct Y {
X mem;
};
上述的类X中没有定义任何拷贝控制成员,且i支持移动操作,std::string也定义了自己的移动操作,因此类Y就会有编译器合成的移动操作:
X x;
X x1 = std::move(x); // 使用合成的移动构造函数
Y y;
Y y1 = std::move(y); // 使用合成的移动构造函数
构造函数的匹配规则
如果一个类即有移动构造函数,也有拷贝构造函数,编译器使用普通的函数匹配规则来确定使用哪个构造函数,赋值操作类似。拷贝构造和函数接收一个const的左值引用,移动构造函数接收一个非const的右值引用:
Test t1, t2
t1 = t2; // t2是左值,使用拷贝赋值
Test getTest(); // getTest返回一个右值
t2 = getTest(); // 使用移动赋值
如果一个类有拷贝构造函数但是没有定义移动构造函数,这种情况下,编译器不会合成移动构造函数,这样这个类就只有拷贝构造函数但是没有移动构造函数,这种情况下,对其显示调用move操作将拷贝对象:
class Foo {
public:
Foo() = default;
Foo(const Foo &); // 拷贝构造函数
// 其他成员,但不定义移动构造函数
... ...
};
Foo x;
Foo y(x); // 拷贝构造函数,x是一个左值
Foo z(std::move(x)); // 拷贝构造函数
对z进行初始化时,调用move(x),它返回一个绑定到x的Foo&&, 此时可以将Foo&&转化为一个const Foo&,因此可以调用拷贝构造函数。
右值引用和成员函数
除了移动构造函数和移动赋值运算符,普通成员函数也可以定义和移动/拷贝构造函数相同参数模式的版本:一个版本接受一个指向const的左值引用,第二个版本接受一个指向非const的右值应用。
例如标准库定义了push_back的两个版本:
void push_back(const X &); // 拷贝
void push_back(X &&); 移动
可以将能转化为类型X的任意对象传递给第一个版本,此版本会从参数拷贝数据。对于第二个版本,只能传递非const的右值。