拷贝会再准备一份资源;移动是把资源的指针交给新对象,旧对象不再持有。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 仍可析构或再赋值,但不要再假定里面还有原来的文字:

C++ std::move 与 string运行
#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:

C++ 移动构造运行
#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 表达式:匿名函数对象与算法配合。