移动语义与右值引用
移动语义是 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 容器的性能至关重要。