外观
Stage07|Modern C++ Foundation
Lesson085|Copy、Move 与值语义
Tags: #C++17 #Copy #Move #ValueSemanticsDifficulty: ⭐⭐⭐⭐⭐
一、问题场景:std::move 为什么没有立刻移动
cpp
std::string a = "texture.png";
auto&& b = std::move(a);std::move 本身不搬运数据,只把表达式转换为可以匹配移动操作的右值类别。上面的 b 仍是对 a 的引用。真正移动发生在某个移动构造或移动赋值被调用时。
二、本课目标
- 理解值语义、身份语义和所有权。
- 区分复制构造、复制赋值、移动构造、移动赋值。
- 理解 Rule of Zero 与 Rule of Five。
- 正确看待移动后对象。
- 识别无意复制和错误移动。
三、回到 TaskSystem:任务应该复制还是移动
提交任务时,如果任务包含字符串、缓冲区或回调,复制可能很贵;如果队列接管它,移动更符合所有权。接口可以设计成按值接收,再移动到队列:
cpp
void submit(Task task) {
tasks_.push(std::move(task));
}调用者传左值时表达“保留原任务”,传右值时表达“交给系统”。移动后源对象仍可析构和重新赋值,但不能继续依赖原内容。
四、复制和移动解决什么
复制创建独立值:
cpp
std::vector<int> a{1, 2, 3};
std::vector<int> b = a;
b[0] = 9; // a 不变移动允许目标接管源对象的资源:
cpp
std::vector<int> c = std::move(a);此后 a 必须仍可析构、可赋值,但其具体内容通常未指定。不要假设一定为空。
五、特殊成员函数
text
析构函数
复制构造函数
复制赋值运算符
移动构造函数
移动赋值运算符如果成员本身都正确管理资源,应优先 Rule of Zero:不手写任何一个,让编译器组合成员语义。
cpp
class MeshData {
std::vector<float> vertices_;
std::string name_;
};只有直接管理低层资源时才考虑 Rule of Five,而且通常更适合先把资源封装进一个 RAII 成员。
六、为什么移动构造常写 noexcept
容器扩容时,如果元素移动可能抛异常,std::vector 为维持强异常保证可能选择复制。资源所有权的移动操作通常可以并应该 noexcept。
cpp
Buffer(Buffer&& other) noexcept;
Buffer& operator=(Buffer&& other) noexcept;七、参数传递策略
text
只读且对象大:const T&
要取得一份副本:按值传入后 move 到成员
可选观察:const T*
明确转移所有权:unique_ptr<T>按值接收并移动适合需要保存参数的 API:
cpp
class Task {
public:
explicit Task(std::string name) : name_(std::move(name)) {}
private:
std::string name_;
};左值调用复制进参数,右值调用移动进参数,接口保持简单。
八、可编译验证:记录特殊成员调用
cpp
#include <iostream>
#include <utility>
#include <vector>
struct Trace {
Trace() = default;
Trace(const Trace&) { std::cout << "copy\n"; }
Trace(Trace&&) noexcept { std::cout << "move\n"; }
};
int main() {
std::vector<Trace> values;
values.reserve(2);
Trace item;
values.push_back(item);
values.push_back(std::move(item));
}预测两次输出。去掉 noexcept,再制造 vector 扩容,观察实现可能选择复制。
九、TaskSystem 的性能判断
不要看到 std::move 就假定性能变好。先确认 Task 的资源是否可转移、vector 是否扩容、lambda 是否复制大对象,再用构造计数和 profiler 验证。返回局部 Task 通常依赖返回值优化,不应为了“强制移动”破坏优化机会。
十、常见误区
text
std::move 会立即清空源对象:错误
移动后对象不能析构:错误
所有参数都用 T&& 更快:错误
复制一定慢:错误,小对象或编译器消除复制时成本很低
返回局部对象必须 std::move:通常错误,可能妨碍返回值优化十一、故障诊断
无意复制时先找构造日志或 profiler 证据,再检查 range-for 是否按值、getter 是否返回副本、lambda 是否复制捕获大对象。移动后崩溃则检查代码是否继续依赖源对象内容,而不是只看它是否为空。
十二、练习与答案
1. const T&& 为什么很少用于转移资源?
移动通常要修改源对象以交出资源,而 const 阻止这种修改,最终往往只能复制。
2. 返回局部 std::vector 是否昂贵?
通常不会。编译器可做返回值优化;即使未消除,也通常可以移动其内部缓冲。
3. 移动后对象可以做什么?
可以析构、重新赋值,并使用类型明确保证的操作;不能假设未规定的旧内容仍存在。
十三、本课总结
复制产生独立值,移动转移可接管的资源。先用 Rule of Zero 获得正确语义,再用测量证明是否需要更精细的性能设计。
