所有文章

C++ 移动语义:从「复制」到「搬家」的思维转变

· 2 分钟

移动语义是 C++11 带来的最重要的特性之一。但我见过很多人(包括早期的我)对它有一个根本性的误解:

移动 ≠ 拷贝一份然后删掉原来的

移动语义的本质是资源所有权的转移。理解了这一点,很多设计决策就会变得自然。

为什么需要移动语义

考虑这个场景:

std::vector<int> createLargeVector() {
    std::vector<int> v(1000000, 42);
    return v;  // C++11 之前:深拷贝!
}

auto vec = createLargeVector();  // 又一次深拷贝

C++11 之前,这里有两次百万元素的拷贝。移动语义让第二次变成 O(1) 的指针交换:

// 移动构造:只是交换内部指针
vector(vector&& other) noexcept
    : data_(other.data_), size_(other.size_), cap_(other.cap_) {
    other.data_ = nullptr;  // 转移所有权
    other.size_ = 0;
    other.cap_  = 0;
}

右值引用:绑定「临时对象」的引用

T&& 叫右值引用,它只能绑定到右值(临时对象、即将消亡的值):

int x = 5;
int&  lref = x;   // ✅ 左值引用
int&& rref = 5;   // ✅ 右值引用(绑定临时值)
int&& bad  = x;   // ❌ 不能绑定左值

关键认知:函数参数 T&& 虽然是右值引用类型,但作为有名字的变量,它本身是左值。这就是 std::move 存在的原因。

std::move:类型转换,不是移动

std::move 什么都没有「移动」,它只是一个类型转换:

// std::move 的本质
template<typename T>
constexpr remove_reference_t<T>&& move(T&& t) noexcept {
    return static_cast<remove_reference_t<T>&&>(t);
}

它把左值转换成右值引用,告诉编译器「这个东西你可以随意取走资源」。

std::string s = "hello";
std::string s2 = std::move(s);  // s 的内部缓冲区被转移到 s2
// s 之后处于「有效但未指定」状态,不能依赖其值

什么时候该用 std::move

✅ 应该用:

  • 在函数内部,把一个局部变量「传出去」,之后不再用它
  • 在移动构造/赋值中,转移成员的所有权

❌ 不该用:

  • return 语句里 return 局部变量:NRVO 已经更好
  • 在函数参数是 const T& 的调用上:毫无意义
  • intdouble 等基本类型:性能没有提升,反而可能阻止优化
// 错误示范:阻止 NRVO
std::string bad() {
    std::string s = "hello";
    return std::move(s);  // ❌ 反而更慢
}

// 正确:让编译器 NRVO
std::string good() {
    std::string s = "hello";
    return s;  // ✅ 编译器直接在返回位置构造
}

完美转发:std::forward

写模板函数时,想把参数「原样」传递给另一个函数:

template<typename T>
void wrapper(T&& arg) {
    // 如果 arg 是左值引用,应该 forward 为左值
    // 如果 arg 是右值引用,应该 forward 为右值
    process(std::forward<T>(arg));
}

std::forward<T>T 是左值引用时什么都不做,在 T 是纯类型时做 std::move

实现正确的移动语义

一个资源管理类的完整实现示例:

class Buffer {
public:
    // 构造
    explicit Buffer(size_t n) : data_(new char[n]), size_(n) {}

    // 拷贝构造:深拷贝
    Buffer(const Buffer& other)
        : data_(new char[other.size_]), size_(other.size_) {
        std::copy(other.data_, other.data_ + size_, data_);
    }

    // 移动构造:O(1),接管所有权
    Buffer(Buffer&& other) noexcept
        : data_(other.data_), size_(other.size_) {
        other.data_ = nullptr;
        other.size_ = 0;
    }

    // 统一赋值运算符(copy-and-swap 惯用法)
    Buffer& operator=(Buffer other) noexcept {
        swap(*this, other);
        return *this;
    }

    ~Buffer() { delete[] data_; }

    friend void swap(Buffer& a, Buffer& b) noexcept {
        using std::swap;
        swap(a.data_, b.data_);
        swap(a.size_, b.size_);
    }

private:
    char*  data_;
    size_t size_;
};

注意移动构造标记了 noexcept,这很重要:std::vector 扩容时只有在移动构造是 noexcept 的情况下才会使用移动而不是拷贝。


移动语义是现代 C++ 中性能优化的基础。理解它需要同时掌握值类别(lvalue/rvalue)、类型推导和资源所有权三个概念。建议结合 Scott Meyers 的《Effective Modern C++》Items 23-30 系统学习。