Skip to content

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 的引用。真正移动发生在某个移动构造或移动赋值被调用时。

二、本课目标 ​

  1. 理解值语义、身份语义和所有权。
  2. 区分复制构造、复制赋值、移动构造、移动赋值。
  3. 理解 Rule of Zero 与 Rule of Five。
  4. 正确看待移动后对象。
  5. 识别无意复制和错误移动。

三、回到 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 获得正确语义,再用测量证明是否需要更精细的性能设计。