Skip to content

移动语义与右值引用

移动语义是 C++11 最重要的特性之一,它让 C++ 在保持零开销抽象的同时,彻底解决了不必要的深拷贝问题。

左值与右值

cpp
int x = 42;      // x 是左值(有名字,有地址)
int y = x + 1;   // x+1 是右值(临时值,无地址)

// 左值引用:绑定到左值
int& lref = x;       // OK
// int& lref2 = 42;  // 错误!不能绑定右值

// 右值引用:绑定到右值(C++11)
int&& rref = 42;     // OK
int&& rref2 = x + 1; // OK

// const 左值引用:可以绑定任何值(C++03 的妥协方案)
const int& cref = 42;  // OK,延长临时对象生命周期

值类别完整图

表达式值类别(C++11)
├── glvalue(泛左值,有身份)
│   ├── lvalue(左值):变量、解引用、成员访问
│   └── xvalue(将亡值):std::move() 的结果、返回右值引用的函数
└── rvalue(右值,可移动)
    ├── xvalue(将亡值)
    └── prvalue(纯右值):字面量、临时对象、lambda

移动构造与移动赋值

cpp
#include <iostream>
#include <utility>

class Buffer {
    size_t size_;
    int*   data_;

public:
    // 构造
    explicit Buffer(size_t n) : size_(n), data_(new int[n]{}) {
        std::cout << "构造 size=" << n << "\n";
    }

    // 析构
    ~Buffer() {
        delete[] data_;
        std::cout << "析构\n";
    }

    // 拷贝构造:深拷贝,O(n)
    Buffer(const Buffer& other) : size_(other.size_), data_(new int[other.size_]) {
        std::copy(other.data_, other.data_ + size_, data_);
        std::cout << "拷贝构造\n";
    }

    // 移动构造:转移所有权,O(1)!
    Buffer(Buffer&& other) noexcept
        : size_(other.size_)
        , data_(other.data_)   // 直接"偷"指针
    {
        other.size_ = 0;
        other.data_ = nullptr;  // 原对象置空,防止 double free
        std::cout << "移动构造\n";
    }

    // 移动赋值
    Buffer& operator=(Buffer&& other) noexcept {
        if (this != &other) {
            delete[] data_;         // 释放自己的资源
            size_ = other.size_;
            data_ = other.data_;
            other.size_ = 0;
            other.data_ = nullptr;
        }
        std::cout << "移动赋值\n";
        return *this;
    }

    size_t size() const { return size_; }
};

Buffer make_buffer(size_t n) {
    return Buffer(n);  // NRVO 优化,可能直接构造在调用方
}

int main() {
    Buffer b1(100);
    Buffer b2 = std::move(b1);  // 移动构造,b1 变为空
    Buffer b3 = make_buffer(200);  // 移动构造(或 NRVO)
}

std::move 的本质

cpp
// std::move 不移动任何东西!
// 它只是一个类型转换:将左值转换为右值引用
// 真正的移动发生在移动构造/移动赋值中

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

// 使用场景
std::string s1 = "hello";
std::string s2 = std::move(s1);  // s1 变为有效但未指定状态
// s1 可能为空,但仍然可以赋值使用

// 常见错误:move 后继续使用
std::vector<int> v1 = {1, 2, 3};
std::vector<int> v2 = std::move(v1);
// v1.size() 可能为 0,不要依赖 move 后的值!

完美转发(Perfect Forwarding)

cpp
#include <utility>

// 问题:如何写一个包装函数,保持参数的值类别?
void process(int& x)  { std::cout << "左值\n"; }
void process(int&& x) { std::cout << "右值\n"; }

// 错误做法:丢失右值信息
template<typename T>
void bad_wrapper(T arg) {
    process(arg);  // arg 是左值!永远调用左值版本
}

// 正确做法:万能引用 + std::forward
template<typename T>
void perfect_wrapper(T&& arg) {  // T&& 是万能引用(forwarding reference)
    process(std::forward<T>(arg));  // 保持原始值类别
}

int main() {
    int x = 42;
    perfect_wrapper(x);         // T=int&,转发为左值
    perfect_wrapper(42);        // T=int,转发为右值
    perfect_wrapper(std::move(x)); // T=int,转发为右值
}

万能引用 vs 右值引用

cpp
// 右值引用:T 是具体类型
void foo(int&& x);          // 只接受 int 右值

// 万能引用:T 是模板参数或 auto
template<typename T>
void bar(T&& x);            // 接受任何值类别

auto&& y = expr;            // auto&& 也是万能引用

// 引用折叠规则(万能引用的底层机制):
// T = int&  → T&& = int& &&  → int&   (左值)
// T = int   → T&& = int&&    → int&&  (右值)
// T = int&& → T&& = int&& && → int&&  (右值)
// 规则:只要有 &,结果就是 &

移动语义的实际收益

cpp
#include <vector>
#include <string>
#include <chrono>

// 场景:从函数返回大容器
std::vector<int> generate(size_t n) {
    std::vector<int> result(n);
    // 填充数据...
    return result;  // NRVO 或移动,不会深拷贝
}

// 场景:容器中存储不可拷贝对象
#include <memory>
std::vector<std::unique_ptr<int>> ptrs;
auto p = std::make_unique<int>(42);
ptrs.push_back(std::move(p));  // unique_ptr 不可拷贝,只能移动

// 场景:字符串拼接优化
std::string build_string(std::string prefix, std::string suffix) {
    prefix += suffix;           // 修改 prefix
    return prefix;              // 移动返回,不拷贝
}

std::string result = build_string("hello, ", "world");

移动语义与 noexcept

cpp
// noexcept 对移动语义至关重要!
// std::vector 在扩容时,只有移动构造是 noexcept 才会使用移动,否则用拷贝

class MyClass {
public:
    // 必须标记 noexcept,否则 vector 扩容时不会移动!
    MyClass(MyClass&&) noexcept = default;
    MyClass& operator=(MyClass&&) noexcept = default;
};

// 验证
#include <type_traits>
static_assert(std::is_nothrow_move_constructible_v<MyClass>);

// vector 扩容行为
std::vector<MyClass> v;
v.reserve(10);
v.push_back(MyClass{});  // 如果移动构造 noexcept,扩容时移动;否则拷贝

返回值优化(RVO/NRVO)

cpp
// RVO(Return Value Optimization):编译器直接在调用方构造对象
// NRVO(Named RVO):具名返回值优化

struct Heavy {
    int data[1000];
    Heavy() { std::cout << "构造\n"; }
    Heavy(const Heavy&) { std::cout << "拷贝\n"; }
    Heavy(Heavy&&) noexcept { std::cout << "移动\n"; }
};

// NRVO:编译器可能直接在 result 的位置构造 h,零拷贝
Heavy make_heavy() {
    Heavy h;    // 直接构造在调用方的栈帧
    return h;   // 不触发移动/拷贝(NRVO)
}

Heavy result = make_heavy();  // 只输出"构造",无拷贝无移动

// C++17 强制 RVO(prvalue 情况)
Heavy result2 = Heavy{};  // 保证零拷贝,不依赖优化

关键认知

移动语义的本质是所有权转移,而非数据复制。std::move 只是类型转换,真正的移动在构造/赋值函数中。始终给移动操作标记 noexcept,这对 STL 容器的性能至关重要。

系统学习 C++ 生态,深入底层架构