拷贝会再准备一份资源;移动是把资源的指针交给新对象,旧对象不再持有。C++11 用右值引用 T&& 和 std::move 表达这种移交。std::string、std::vector、unique_ptr 都支持移动,所以 push_back(std::move(s)) 不必再复制整段文字。本节先区分左值与右值,再给出可运行的 std::move,并说明类里的移动构造。五法则(Rule of Five)指:一旦自己管理资源,析构、拷贝构造、拷贝赋值、移动构造、移动赋值这五个函数要一起考虑。先复习引用与智能指针。
左值与右值
有名字、能取地址的是左值,例如变量 s。没有持久名字的临时量是右值,例如字面量 42、函数按值返回的临时 std::string。T& 只能绑左值;T&& 绑右值,表示可以放手把资源拿走。const T& 也能绑右值,用来只读延长临时对象寿命,但不能通过它改内容。
| 表达式 | 类别(入门) |
|---|---|
s、v[0] | 左值 |
42、std::string("hi") | 右值 |
std::move
std::move(s) 并不移动任何东西,只是把左值 s 转成右值,让接收方可以走移动而不是拷贝。移动之后 s 仍可析构或再赋值,但不要再假定里面还有原来的文字:
#include <iostream>
#include <string>
#include <utility>
#include <vector>
int main() {
std::string s = "long text";
std::vector<std::string> bucket;
bucket.push_back(std::move(s)); // 把 s 里的存储交给 vector,不再拷贝整串
std::cout << s.size() << "\n";
std::cout << bucket[0] << "\n";
return 0;
}输出(常见实现里被移走的 string 长度为 0):
0
long text已经会用的 unique_ptr 也是同一规则:不能拷贝,只能 std::move。
移动构造
自己用 new[] 管一块内存时,移动构造应把指针拿走,并把对方的指针置空,避免两个对象析构时 delete[] 同一块。下面把 a 移给 b 后,a 为空,b 里仍是原先的 7:
#include <iostream>
#include <utility>
class Buffer {
int* p_ = nullptr;
int n_ = 0;
public:
Buffer(int n, int fill) : p_(new int[n]), n_(n) {
for (int i = 0; i < n; ++i) {
p_[i] = fill;
}
}
Buffer(const Buffer&) = delete;
Buffer& operator=(const Buffer&) = delete;
Buffer(Buffer&& other) noexcept : p_(other.p_), n_(other.n_) {
other.p_ = nullptr; // 避免析构时重复 delete[]
other.n_ = 0;
}
~Buffer() { delete[] p_; }
int first() const { return p_[0]; }
bool empty() const { return p_ == nullptr; }
};
int main() {
Buffer a(2, 7);
Buffer b = std::move(a);
std::cout << b.first() << "\n";
std::cout << a.empty() << "\n";
return 0;
}输出:
7
1移动操作常标 noexcept,vector 扩容时才敢用移动而不是拷贝。成员若已是 string / vector / 智能指针,编译器合成的移动通常已经够用,不必手写。
五法则(Rule of Five)
类有五个与拷贝、移动、销毁相关的特殊成员函数:
| 函数 | 何时用到 |
|---|---|
| 析构函数 | 对象结束时释放资源 |
| 拷贝构造 | Buffer b = a;(a 还要用) |
| 拷贝赋值 | b = a; |
| 移动构造 | Buffer b = std::move(a); |
| 移动赋值 | b = std::move(a); |
C++98 的规则三是:只要自己写了其中的析构或拷贝,另外几个拷贝相关函数也要写,否则编译器合成的浅拷贝会和手写析构打架(两份对象 delete[] 同一块)。C++11 加上移动之后变成五个,所以叫五法则:这五个要么自己写全,要么用 = delete 明确禁止(像上面的 Buffer 禁止拷贝、只保留移动),不要只写析构而留下默认浅拷贝。
成员已经把资源管好时,五个都不要手写,这叫零法则(Rule of Zero),日常类应优先这样设计。
了解:返回值优化
按值返回局部对象时,编译器常常直接在调用方的位置构造,连移动都省掉(RVO)。不要对即将 return 的局部变量写 std::move,有可能反而妨碍这种优化。
本节说明了右值引用与 std::move 的入门含义。下一节介绍Lambda 表达式:匿名函数对象与算法配合。