目录
13.6.1 右值引用
右值引用 - 必须绑定到右值的引用
通过 & 获得左值引用(常规引用),通过 && 获得右值引用
int i = 42;
int &r = i; // 正确:r 引用 i
int &&rr = i; // 错误:不能将一个右值引用绑定到一个左值上
int &r2 = i * 42; // 错误:不能将一个左值引用绑定到一个右值上
int const&r3 = i * 42; // 正确:可以将一个 const 引用绑定到一个右值上
int &&rr2 = i * 42; // 正确:rr2 引用 乘法结果
(1) 左值持久,右值短暂
左值有持久的状态,而右值要么是字面常量,要么是表达式求值过程中创建的临时变量
字面常量:‘a’,42,“Hello,World!”
临时变量:5 * 36
由于右值引用只能绑定到临时对象 - 右值,知右值引用的两个特性:
右值引用的对象将要被销毁
该对象没有其他用户
这两个特性意味着:使用右值引用的代码可以自由接管所引用的对象的资源
(2) 变量是左值
变量是左值,不能绑定到右值引用上,所以我们很少犯如下错误:
int i = 42;
int &&r = i;
但是注意:不能将一个右值引用绑定到另一个右值引用类型的变量上,如下所示:
int &&rr1 = 42;
int &&rr2 = rr1; // 错误:右值引用变量 rr1 是左值
int &r1 = i;
int &r2 = r1; // 正确:引用的引用
(3) 标准库 move 函数
虽然不能将一个右值引用直接绑定到一个左值上,但可以显式地将一个左值转换为对应的右值引用类型
通过 move 函数来获得绑定到左值上的右值引用,此函数定义在头文件 utility 中
int &&rr1 = 42;
int &&rr2 = std::move(rr1);
move 调用告诉编译器:我们有一个左值,但我们希望像一个右值一样处理它
调用 move 意味着承诺:除了对左值(rr1)赋值或销毁它外,我们将不再使用它的值(特别针对类类型)
13.6.2 移动构造函数和移动赋值运算符
(1) 移动构造函数
移动构造函数第一个参数是一个右值引用,任何额外参数必须有默认实参
移动构造函数必须确保移后源对象处于这样一个状态:销毁它是无害的
注意:一旦资源完成移动,源对象必须不再指向被移动的资源,这些资源的所有权已经归属新创建的对象
为 my_class 类定义移动构造函数,如下所示:
class my_class {
_Ty* resource;
public:
my_class() = default;
my_class(my_class&&)noexcept; // 移动操作不应抛出任何异常
~my_class();
};
my_class::~my_class() {
if (resource)
delete resource;
}
my_class::my_class(my_class&& s)noexcept
// 接管 s 中的资源
:resource(s.resource) {
// 令 s 进入这样的状态:对其运行析构函数是安全的
s.resource = nullptr;
}
注意:不抛出异常的移动构造函数和移动赋值运算符必须标记为 noexcept
标准库默认认为移动构造函数在移动类对象时可能会抛出异常,并且为了处理这种可能性而做一些额外的工作
(2) 移动赋值运算符
注意:检测自赋值
my_class& my_class::operator=(my_class&& s)noexcept{
// 直接检测自赋值
if (this != &s) {
if (resource)
delete resource; // 释放已有资源
resource = s.resource; // 从 s 接管资源
if (s.resource)
delete s.resource; // 将 s 置于可析构状态
}
return *this;
}
进行自赋值检查的原因:
右值可能是 move 调用的结果,不能在使用右侧运算对象资源之前就释放左侧运算对象资源(可能是相同的资源)
(3) 移后源对象必须可析构
在移动操作之后,移后源对象必须保持有效的、可析构的状态,但是用户不能对其值有任何假设
有效的:可以安全地为其赋新值或者可以安全地使用而不依赖其当前值
不能对其值有任何假设:当我们从标准库 string 移动数据时,我们可能期待一个移后源对象是空的,但是对它执行 empty 或 size 这些操作的结果并没有保证
(我们的程序不应该依赖于移后源对象中的数据)
(4) 合成的移动操作
如果不声明自定义的拷贝构造函数或拷贝赋值运算符,编译器总会自动合成这些操作
与拷贝操作不同,编译器不会为某些类合成移动操作:
- 编译器不会为定义了拷贝构造函数、拷贝赋值运算符和析构函数的类合成移动操作
- 当一个类的某些数据成员不能移动构造或移动赋值时,编译器不会为它合成移动操作
如果一个类没有移动操作,通过正常的函数匹配,类会使用拷贝操作来代替移动操作
struct X {
int i; // 内置类型可以移动
std::string s; // string 定义了自己的移动操作
}; // 编译器会为 X 合成移动操作
struct hasX {
X mem; // X 有合成的移动操作
}; // 编译器会为 hasX 合成移动操作
// 使用合成的构造函数
X x; hasX hx;
X x2 = std::move(x);
hasX hx2 = std::move(hx);
编译器合成移动操作的条件:
一个类没有定义移动操作
一个类没有定义任何自己版本的拷贝控制成员
一个类的所有数据成员都能移动构造或移动赋值(无论这些移动操作是自定义的还是合成的)
一个类没有定义析构函数
例:定义析构函数阻止合成移动操作
struct my_class {
my_class() = default;
my_class(my_class const&) {
cout << "copy-constructor" << endl;
}
~my_class() {}
};
int main() {
my_class x;
my_class x1 = std::move(x);
}
运行结果:
copy-constructor
定义析构函数阻止了合成移动操作,类使用拷贝操作来代替移动操作
与拷贝操作不同,移动构造函数永远不会隐式定义为删除的函数
如果显式地要求编译器生成 =default 的移动操作且编译器不能移动所有成员,则编译器会将移动操作定义为删除的函数
=default 将移动操作定义为删除的函数的例子:
有类成员定义了拷贝构造函数且未定义移动构造函数
有类成员未定义拷贝构造函数且编译器不能为其合成移动操作
有类成员的移动操作被定义为删除的或不可访问的
有类成员是 const 的或是引用
例:Y 是一个类,它定义了拷贝构造函数但未定义移动构造函数
struct hasY {
Y mem;
hasY() = default;
hasY(hasY&&) = default; // =default 将 hasY 的移动构造函数定义为删除的函数
};
hasY hy;
hasY hy2 = std::move(hy); // 移动构造函数是删除的,使用拷贝操作来代替移动操作
注意:定义了拷贝操作的类不会合成移动操作,定义了移动操作的类对拷贝操作的合成也有影响
定义了移动操作的类,合成的拷贝操作会被定义为删除的
综上:定义拷贝影响合成移动,定义移动影响合成拷贝,同时定义拷贝和移动以确保类对象正常工作
(5) 移动右值,拷贝左值
如果一个类既有移动构造函数,又有拷贝构造函数,编译器使用普通的函数匹配规则来确定使用哪个构造函数
struct my_class {
my_class() = default;
my_class& operator=(my_class const&) {
cout << "copy-constructor" << endl;
return *this;
}
my_class& operator=(my_class&&) {
cout << "move-constructor" << endl;
return *this;
}
};
my_class create() {
return my_class();
}
int main() {
my_class mc1, mc2;
mc1 = mc2; // mc2 是左值,使用拷贝构造
mc2 = create(); // create() 是右值,使用移动构造
}
运行结果:
copy-constructor
move-constructor
如果没有移动构造函数,右值也被拷贝
struct Foo {
Foo() = default;
Foo(Foo const&) {
cout << "copy-constructor" << endl;
}
};
int main() {
Foo x;
Foo y(x); // x 是左值,使用拷贝构造
Foo z(std::move(x)); // 未定义移动构造函数,拷贝右值 std::move(x)
}
运行结果:
copy-constructor
copy-constructor
(6) 三五法则
三:拷贝构造函数、拷贝赋值运算符、析构函数
五:拷贝构造函数、拷贝赋值运算符、析构函数、移动构造函数、移动赋值运算符
如果一个类定义了任何一个拷贝操作,它就应该定义所有五个操作
(7) 移动迭代器(move iterator)
移动迭代器的解引用运算符生成一个右值引用
通过调用标准库函数 make_move_iterator 将一个普通迭代器转换为一个移动迭代器
注意:标准库不保证哪些算法使用移动迭代器,哪些不适用
建议:在移动构造函数和移动赋值运算符的实现代码之外的地方,不要随意使用移动操作
13.6.3 右值引用和成员函数
除了构造函数和赋值运算符之外,一个成员函数同时提供拷贝和移动版本也有好处
这种函数通常一个版本接受指向 const 的左值引用,另一个版本接受指向非 const 的右值引用
(1) 引用限定符
在 C++ 旧标准中,没有办法阻止对右值进行赋值,如下所示:
string s1, s2;
s1 + s2 = "wow!";
在此情况下,我们希望阻止这种用法,即希望强制左侧运算对象是一个左值
在参数列表后放置一个引用限定符,强制 this 指向对象的左值/右值属性
class my_string {
char* data;
size_t size, capacity;
public:
// 只可用于非 const 的左值
my_string& operator=(my_string const& _Right)&;
};
引用限定符可以是 & 或 &&,分别指出 this 指向一个左值或右值:
// 可用于非 const 的右值
my_string& my_string::operator=(my_string const& _Right)&&;
成员函数可以同时用 const 和引用限定,但引用限定符必须在 const 限定符之后:
// 可用于任何类型的 my_string
my_string& my_string::operator=(my_string const& _Right)const&;
注意:引用限定符对于构造函数/析构函数非法
(2) 重载和引用函数
引用限定符可以区分成员函数的重载版本,如下所示:
class my_vector {
int* data;
size_t size, capacity;
public:
my_vector sorted()&&; // 可用于非 const 的右值
my_vector sorted()const&; // 可用于任何类型的 my_vector
};
// this 指向右值,没有其他用户,可以原址排序
my_vector my_vector::sorted()&& {
sort(data, data + size);
return *this;
}
// this 指向 const 对象或左值,不能原址排序
my_vector my_vector::sorted()const& {
my_vector ret(*this);
sort(ret.data, ret.data + size);
return ret;
}
my_vector().sorted(); // 精确匹配参数为非 const 的右值的重载版本
my_vector nums;
nums.sorted(); // 匹配参数为 const& 的泛用重载版本
注意:如果一个成员函数有引用限定符,则具有相同参数列表的所有重载版本都必须有引用限定符