右值和右值引用

左值可以位于赋值语句的左边,但右值不行,比如一个常量,只能出现在右边

通过 && 获得右值引用,右值引用只能绑定到有个即将销毁的对象,因此可以将一个右值引用的资源 移动 到另一个对象中

1
2
3
4
5
6
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 绑定到乘法结果上

标准库 move

上面提到过不能将右值引用绑定到左值上,但可以显式地将左值转换为对应的右值引用类型,还可以通过 move 的新标准库函数来获得绑定到左值上的右值引用

1
2
3
4
5
#include <utility>

int &&rr1 = 42; // 正确,字面常量是右值
int &&rr2 = rr1; // 错误,表达式 rr1 是左值
int &&rr3 = std::move(rr1); // ok,对 move 不使用 using 声明,直接使用 std::move, 避免潜在名字冲突

int &&rr3 = std::move(rr1); 使用 std::move 函数将 rr1 的所有权转移给了 rr3。这意味着 rr3 现在拥有了原本属于 rr1 的资源或对象。调用 move 意味着承诺:除了对 rr1 赋值或销毁它外,将不再使用它。

注意std::move 函数并不实际移动对象,它只是将对象标记为可移动的。真正的移动操作是在移动构造函数或移动赋值运算符中执行的。

移动构造函数

1
2
3
4
5
6
StrVec::StrVec(StrVec &&s) noexcept // 移动操作不会抛出任何异常
// 成员初始化器接管 s 中的资源
: elements(s.elements), first_free(s.first_free), cap(s.cap)
{
s.elements = s.first_free = s.cap = nullptr;
}

移动构造函数不分配任何新内存,而是接管给定的 StrVec 中的内存,接管内存后,将给定对象中的指针都置为 nullptr,这就完成了从给定对象的移动操作,此对象将继续存在。最终移后源对象会被销毁,意味着将在其上运行析构函数,如果忘记了改变 s.first_free,则销毁移后源对象会释放掉我们刚刚移动的内存。

移动赋值运算符

1
2
3
4
5
6
7
8
9
10
11
StrVec &StrVec::operator=(StrVec &&rhs) noexcept {
if (this != &rhs) {
free(); // 释放已有元素
elements = rhs.elements; // 从 rhs 接管资源
first_free = rhs.first_free;
cap = rhs.cap;
rhs.elements = rhs.first_free = rhs.cap = nullptr;
}

return *this;
}

移动迭代器

新标准库定义了移动迭代器适配器,通过调用 make_move_iterator 将一个普通迭代器转换为一个移动迭代器,一个迭代器的解引用运算符返回一个指向元素的左值,与其他迭代器不同,移动迭代器的解引用运算符生成一个右值引用

1
2
3
auto last = uninitialized_copy(make_move_iterator(begin()),
make_move_iterator(end()),
first);

右值引用和成员函数

如果一个成员函数同时提供拷贝和移动版本,那么可以定义两个版本,一个是指向 const 的左值引用,第二个是指向非 const 的右值引用

1
2
void push_back(const T&); // 拷贝,绑定到任意类型 T
void push_back(T&&); // 移动,只能绑定到类型 T 的可修改的右值