一、移动语义的核心原理
移动语义解决了C++03时代的一个根本问题:按值传参和返回值时的深拷贝代价。理解右值引用(&&)和移动构造/赋值,是写出零开销抽象的基础。
1.1 左值 vs 右值 vs 亡值
// C++11将表达式分为三类:
//
// ① 左值(lvalue):有持久地址,可取地址
// int x; x = 5; // x是左值
// std::string s; s = "hello"; // s是左值
//
// ② 亡值(xvalue):即将销毁的对象,可以"抢走"资源
// std::move(x) // 转为xvalue
// std::unique_ptr(new int(42)) // 临时智能指针
//
// ③ 纯右值(prvalue):即将销毁的临时对象
// 42 + 1 // 纯右值
// func_returning_string() // 临时string
// 移动构造:只"偷"指针,不深拷贝
class Buffer {
char* data_;
size_t size_;
public:
// 移动构造:接管源对象的资源,原对象析构时不会重复释放
Buffer(Buffer&& other) noexcept
: data_(other.data_), size_(other.size_) {
other.data_ = nullptr; // 源对象析构时data_为nullptr,不释放
other.size_ = 0;
}
// 移动赋值:先释放自己的资源,再"偷"别人的
Buffer& operator=(Buffer&& other) noexcept {
if (this != &other) {
delete[] data_; // 先释放自己
data_ = other.data_; // 再"偷"
size_ = other.size_;
other.data_ = nullptr;
}
return *this;
}
};
// Benchmark:移动 vs 拷贝
// std::vector v1(1000000, 1);
// v2 = v1; // 深拷贝:~10ms
// v3 = std::move(v1); // 移动:~0.01ms ← 1000倍差距!
二、完美转发与通用引用
2.1 T&&不是总是右值引用
// 模板参数推导中的T&&:
template
void func(T&& x) {
// T=int, x是int&&(右值引用)
// T=int&, x是int&(左值引用!)
// ⚠️ T&& 在模板中被称为"通用引用"
// 它可以是左值也可以是右值,取决于传入的参数
// forward(x):完美转发,保持原始值类别
g(std::forward(x));
}
int a;
func(a); // T=int&, 调用g(int&)
func(std::move(a)); // T=int, 调用g(int&&)
template
void dispatch(std::string name, Args&&... args) {
// args是通用引用包
// 用std::forward_as_tuple完美转发所有参数
Executor::get().execute(name, std::forward_as_tuple(args...));
}
2.2 返回值优化(RVO/NRVO)
// ⚠️ 常见误区:return std::move(obj)反而可能禁用RVO
struct BigData {
std::vector v;
BigData() { v.reserve(1000000); } // 构造函数
BigData(const BigData&) { /* 深拷贝 */ } // 拷贝构造
BigData(BigData&&) noexcept { /* 移动 */ } // 移动构造
};
// 编译器强制RVO(NRVO):
// 如果返回值是局部对象,编译器可以在返回值位置直接构造
BigData create() {
BigData d; // ← 编译器直接在这里构造返回值
d.v.push_back(1);
return d; // 无拷贝无移动
}
// → 即使有移动/拷贝构造函数,也不会调用
// ⚠️ return std::move(d) 可能害处:
// 禁用NRVO → 变成"拷贝/移动到返回值"
// 只在函数返回局部对象的引用时可能有用
// (但这本身是悬垂引用的危险操作)
三、智能指针与移动语义
#include
// unique_ptr:独占所有权的智能指针,禁止拷贝
std::unique_ptr p1(new int(42));
// std::unique_ptr p2 = p1; // ❌ 编译错误
std::unique_ptr p2 = std::move(p1); // ✅ 移动所有权
// p1变为空,p2持有int(42)
// unique_ptr用作类成员时的最佳实践
class Service {
std::unique_ptr config_; // ✅ 独占
std::shared_ptr db_; // ✅ 可共享
public:
Service() : config_(std::make_unique()) {}
void set_database(std::shared_ptr db) {
db_ = std::move(db); // 接受shared_ptr所有权
}
};
// shared_ptr的移动(引用计数不变,转移所有权)
std::shared_ptr sp1 = std::make_shared();
// shared_ptr sp2 = sp1; // 引用计数+1
std::shared_ptr sp2 = std::move(sp1); // 引用计数不变!
// sp2.use_count() = 1, sp1.use_count() = 0, sp1=nullptr
四、emplace与原地构造
// push_back vs emplace的性能差异
std::vector v;
// push_back:先构造临时string,再拷贝/移动到容器
v.push_back(std::string("hello")); // 构造 + 移动
// emplace_back:直接在容器内构造,省去中间步骤
v.emplace_back("hello"); // 直接构造,无中间对象
// ⚠️ emplace_back的类型推导问题
struct Point { int x, y; };
std::vector points;
points.emplace_back(1, 2); // 正确:就地构造Point(1,2)
// ❌ 陷阱:
points.emplace_back({1, 2}); // 先构造initializer_list,再从initializer_list构造Point
// 实际调用:Point(initializer_list),不是Point(int, int)!
// 正确做法:
points.emplace_back(1, 2); // ✅ 推荐
points.push_back(Point{1, 2}); // ✅ 显式构造
points.push_back({1, 2}); // ⚠️ 可能有歧义
// 性能测试(100万次插入):
// push_back(string): 120ms(1次构造+1次移动)
// emplace_back: 45ms(1次构造) ← 2.7倍提速