第十九章 移动语义与完美转发(进阶核心)

← 返回目录

本章目标:彻底搞懂左值/右值、移动构造/移动赋值、 std::move、std::forward、引用折叠、完美转发。 这是"会写 C++"和"懂 C++"的分水岭,请反复读两遍。

19.1 值类别:左值 vs 右值(一切的起点)

人话定义:

int a = 42;        // a 是左值;42 是右值(字面量)
a + 1;             // 表达式 a+1 的结果是右值(临时值)
&a;                // 合法:左值有地址
// &42;            // 非法:右值没有地址
std::string s = "hi";   // s 是左值;"hi" 是右值

关键区分:

int foo() { return 1; }        // foo() 是右值
int& bar() { static int x = 1; return x; }   // bar() 是左值

19.2 右值引用:绑定到"临时对象"的引用

int&& r = 42;        // 右值引用:只能绑定右值
int x = 10;
// int&& r2 = x;     // 错误!x 是左值

右值引用存在的意义:**能安全地"偷走"临时对象的资源**。 因为临时对象马上就要销毁,拿走它的东西不会影响任何"人"。

所以 C++11 给类型系统加了右值引用,并由此引出移动语义。

19.3 移动语义:资源搬家而不是拷贝

拷贝:深复制(新内存 + 复制内容),O(n),安全但慢。 移动:偷指针(交接资源),O(1),快但原对象变"空壳"。

std::string s1 = "hello";
std::string s2 = std::move(s1);   // 把 s1 的资源搬给 s2
// s1 现在内容未指定(通常是空),但合法可析构

标准库大量使用移动:

std::vector<std::string> v;
v.push_back(std::string(1000, 'x'));   // 临时对象 → 移动进容器

std::move(x) 做了什么?人话: 它不移动任何东西!它只是把 x 标记成"可以被移动"(转成右值), 真正的移动发生在"移动构造/移动赋值"里。 std::move 就是一个 cast:static_cast<T&&>(x)。

19.4 自己写移动构造/移动赋值

class Buffer {
public:
    // 拷贝构造(深复制)
    Buffer(const Buffer& other)
        : size_(other.size_), data_(new char[other.size_]) {
        std::copy(other.data_, other.data_ + size_, data_);
    }
    // 移动构造(偷资源,O(1))
    Buffer(Buffer&& other) noexcept
        : size_(other.size_), data_(other.data_) {
        other.data_ = nullptr;    // 原对象必须置空!
        other.size_ = 0;
    }
    // 移动赋值
    Buffer& operator=(Buffer&& other) noexcept {
        if (this != &other) {              // 防止自赋值
            delete[] data_;                // 释放自己的旧资源
            data_ = other.data_;
            size_ = other.size_;
            other.data_ = nullptr;
            other.size_ = 0;
        }
        return *this;
    }
    ~Buffer() { delete[] data_; }
private:
    char* data_ = nullptr;
    std::size_t size_ = 0;
};

移动语义三条铁律:

  1. 移动操作必须 noexcept(标准库 vector 扩容时,

没有 noexcept 就退回拷贝——性能白搭)

  1. 移动后原对象必须处于"合法但未指定"状态(能析构、能赋值)
  2. 移动操作转移"资源所有权",不是复制内容

19.5 什么时候会自动移动?

自动移动的场景(写了移动构造才有意义):

  1. 返回局部对象:std::string f() { std::string s; return s; }

(实际上现代编译器多数直接 RVO/NRVO 省略拷贝,连移动都省了)

  1. 把右值传给"按值"参数:void f(std::string s); f(make_str());
  2. push_back/emplace_back 右值参数
  3. std::move 显式标记的
  4. 临时对象返回 + vector 扩容(前提:noexcept)

不自动移动的场景(坑):

想要移动:s2 = std::move(s1);

19.6 std::forward 与完美转发

场景:写一个包装函数,把参数"原样"转发给另一个函数, 要求:传进来的如果是左值就按左值传,右值就按右值传。

void inner(std::string&)  { /* 左值版本 */ }
void inner(std::string&&) { /* 右值版本 */ }

template <typename T>
void wrapper(T&& arg) {          // T&& 是"转发引用"
    inner(std::forward<T>(arg)); // 完美转发
}

std::string s = "x";
wrapper(s);                 // 转发成左值 → inner(string&)
wrapper(std::move(s));      // 转发成右值 → inner(string&&)

关键知识:T&&(模板参数推导的&&)叫"转发引用"或"万能引用", 不是普通的右值引用!它既能绑定左值也能绑定右值:

引用折叠规则(记结论就行):

T&  &   → T&
T&  &&  → T&
T&& &   → T&
T&& &&  → T&&
只要有一个 & 就折叠成 &;两个 && 才折叠成 &&。

std::forward<T>(arg) 的机制:根据 T 是引用还是值, 决定把 arg 转成左值还是右值。人话:**forward = "参数进来是 啥类别,就保持啥类别传给下一个"**。

为什么不用 std::move?move 无条件转右值—— 如果原参数是左值也被转成右值,就破坏了"原样"。

19.7 完美转发的实际用途

  1. make_unique / emplace_back 都是完美转发:
   template <typename T, typename... Args>
   unique_ptr<T> make_unique(Args&&... args) {
       return unique_ptr<T>(new T(std::forward<Args>(args)...));
   }
  1. 装饰器/包装器:给任意函数加日志、计时、加锁
  2. 工厂函数:统一创建接口

19.8 一个完整的实战例子:计时包装器

template <typename F, typename... Args>
auto time_it(const char* name, F&& f, Args&&... args) {
    auto start = std::chrono::steady_clock::now();
    auto result = std::forward<F>(f)(std::forward<Args>(args)...);
    auto end = std::chrono::steady_clock::now();
    std::println("{} 耗时 {} 微秒", name,
                 std::chrono::duration_cast<std::chrono::microseconds>
                     (end - start).count());
    return result;
}

// 用法
std::vector<int> v(1'000'000);
std::iota(v.begin(), v.end(), 0);
auto mx = time_it("求最大值", [&] {
    return *std::ranges::max_element(v);
});

forward<F>(f):函数对象本身也可能需要移动/拷贝转发。 (完整可运行版本见配套测试文件。)

19.9 返回值优化 RVO:比移动还快

std::string make() {
    std::string s = "hi";
    return s;          // 编译器直接构造在调用方的目标位置
}                      // 不拷贝、不移动、零开销!

条件:返回局部变量本身(不能是条件表达式挑一个返回)。 C++17 起 RVO 是"保证"的,不再是优化可选。

"移动并不总是发生"是性能优化的重要一环—— 能 RVO 就不 move,能 move 就不 copy。

19.10 常见坑清单

  1. std::move 之后还继续用原对象 → 内容可能已空(未定义行为边界)
  2. 移动操作没标 noexcept → vector 扩容时白白拷贝
  3. 忘了把被移动对象置空 → 两个对象共享同一资源 → 双重释放崩溃
  4. 自赋值:s = std::move(s);(移动赋值里检查 this != &other)
  5. 把 move 当成"性能神药"到处乱用——左值上 move 反而破坏状态
  6. 移动构造函数里调用"会分配内存"的操作 → 移动的意义没了
  7. 泛型代码里该用 forward 却用了 move → 参数类别被破坏

本章小结

练习题(配套测试:测试_第19章_移动语义.cpp)


  1. 写一个类,带打印的拷贝/移动构造,观察各操作触发时机。
  2. 实现完整 Buffer(含移动赋值)并通过所有测试。
  3. 写完美转发的日志包装器。
  4. 比较"返回局部对象"在 C++17 下的行为(应零拷贝)。