跳到主要内容

移动语义与右值引用实战

移动语义是 C++11 最重要的性能特性,但也是最容易被"乱用"的特性。核心结论先说:移动语义解决的是"把即将销毁的对象的资源偷过来",不是"加速所有赋值"。

一、值类别速记​

  • 左值(lvalue):有名字、可取地址,如变量、*p、v[0];
  • 将亡值(xvalue):生命周期即将结束的对象,如 std::move(x) 的结果;
  • 纯右值(prvalue):临时量,如 1 + 2、Foo()。

左值 + 将亡值统称泛左值,将亡值 + 纯右值统称右值。移动构造绑定的是右值。

二、右值引用与移动构造​

class Buffer {
public:
explicit Buffer(std::size_t n) : data_(new char[n]), size_(n) {}
~Buffer() { delete[] data_; }

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

// 移动构造:接管指针
Buffer(Buffer&& o) noexcept : data_(o.data_), size_(o.size_) {
o.data_ = nullptr;
o.size_ = 0;
}

Buffer& operator=(Buffer&& o) noexcept {
if (this != &o) {
delete[] data_;
data_ = o.data_;
size_ = o.size_;
o.data_ = nullptr;
o.size_ = 0;
}
return *this;
}

private:
char* data_ = nullptr;
std::size_t size_ = 0;
};

注意 noexcept:移动构造若不声明 noexcept,std::vector 扩容时会退化为拷贝,性能优势消失。

三、std::move 只做类型转换​

std::move 不移动任何东西,它只是把左值转成右值引用:

std::string a = "hello";
std::string b = std::move(a); // 真正的"移动"发生在 b 的移动构造里
// 此后 a 处于"有效但未指定"状态,只能赋值或析构,不要读取

对没有移动构造的类型(只定义了拷贝构造),std::move 完全无效果,仍然走拷贝。对 int、double、裸指针等平凡类型,移动等于拷贝。

四、移动的典型收益场景​

场景 1:容器扩容​

std::vector 扩容时,如果元素类型的移动构造是 noexcept 的,就移动而非拷贝元素。这就是自定义类型务必标 noexcept 的原因。

场景 2:从函数返回大对象​

现代编译器有 RVO/NRVO,直接返回局部变量通常不产生拷贝,不用刻意写 return std::move(x);——反而会阻止 NRVO,属于常见错误。

std::vector<int> make() {
std::vector<int> v(1000);
return v; // ✅ NRVO
// return std::move(v); // ❌ 阻碍 NRVO,可能更慢
}

场景 3:容器插入​

std::vector<std::string> v;
std::string s = "long string ...";
v.push_back(std::move(s)); // 所有权转移,避免复制
v.emplace_back("in place"); // 就地构造,同样避免临时对象

emplace_back 直接从参数构造元素,一般优于 push_back(T(...))。

五、五法则与零法则​

定义析构、拷贝构造、拷贝赋值、移动构造、移动赋值中的任意一个,通常需要重新审视全部五个(五法则)。更推荐零法则:让成员(如 std::string、std::vector、std::unique_ptr)自己管理资源,你就一行都不用写。

只有在封装裸资源(文件句柄、sqlite3*、malloc 内存)时才手写移动操作,并立即考虑用 RAII 包装类收口。

六、完美转发与万能引用​

template <class T>
void relay(T&& arg) { // T&& 在推导语境下是"万能引用"
target(std::forward<T>(arg));
}
  • 传入左值时 T 推导为 T&,std::forward 保持左值;
  • 传入右值时 T 推导为 T,std::forward 转成右值。

如果不用 std::forward 而直接传 arg,参数永远以左值形式传递,右值实参的移动语义就丢了。

template <class... Args>
std::unique_ptr<Foo> makeFoo(Args&&... args) {
return std::unique_ptr<Foo>(new Foo(std::forward<Args>(args)...));
}

七、常见误用清单​

误用后果正确做法
对 const 对象 std::move静默退化拷贝移动前确认对象非 const
return std::move(local)阻碍 NRVO直接 return local
移动后继续读取原对象未定义行为(标准为"未指定状态")移动后只赋值或析构
移动构造未标 noexcept容器扩容退化为拷贝加 noexcept
对 int/指针大量 std::move无收益,噪声直接赋值

八、自查清单​

迁移老代码时按顺序检查:

  1. 管理裸资源(new/malloc/句柄)的类是否补了移动构造与移动赋值,并标 noexcept;
  2. 是否有大量"构造临时对象再插入容器"的地方,可改用 emplace;
  3. 是否有 return std::move(x) 的错误写法;
  4. 是否有移动后仍被读取的对象;
  5. 移动构造里是否把源对象置为可安全析构的状态(指针置空)。

移动语义的价值在于"让资源流动一次",而不是"让每行代码看起来更快"。