C++语义copy and swap示例详解 前言copy-and-swap拷贝并交换是 C 里实现赋值运算符的经典惯用法。它的核心思想只有一句话先完整地拷贝出一份副本再用一个不抛异常的swap把副本和当前对象交换。副本在函数结束时析构顺带释放旧资源。这个惯用法经常被误传成「赋值运算符必须这么写」。实际上它不是语法要求而是一种在「异常安全」「自赋值安全」「代码复用」三者之间取得平衡的设计选择。它也有代价即使当前对象的容量足够也可能先分配一份新内存。另一个常见误解是把它和移动语义对立起来。实际上按值传参的 copy-and-swap 可以同时服务拷贝赋值和移动赋值传左值时参数被拷贝传右值时参数被移动函数体只有一份。本文从朴素赋值运算符的问题讲起给出完整的 copy-and-swap 实现解释它为什么能提供强异常保证最后说明适用边界和常见误区。代码以 C17 为基准。一、朴素赋值运算符的三个问题先看一个管理动态数组的类最直觉的赋值写法是// 适用于 C17反面示例不推荐class Buffer {public:Buffer operator(const Buffer other) {if (this other) return *this; // 自赋值检查delete[] data_; // 先释放旧资源size_ other.size_;data_ new int[size_]; // 若这里抛异常对象已被破坏std::copy(other.data_, other.data_ size_, data_);return *this;}// ...};这段代码有三个问题异常安全差。delete[]已经执行、new却抛出std::bad_alloc时对象处于「data_悬垂、size_已改」的坏状态连析构都可能二次释放。这连基本保证都做不到。自赋值检查容易漏。如果漏掉if (this other)delete[] data_之后other.data_也悬垂随后拷贝就是释放后使用属于 UB。拷贝赋值和移动赋值要写两份逻辑高度重复容易改一处漏一处。copy-and-swap 用「先拷贝、后交换」的顺序把「可能抛异常的操作」全部提前到对象被修改之前从而一次性解决前两个问题按值传参又顺带解决了第三个。二、完整实现下面是一个管理int数组的Buffer实现了规则五析构、拷贝构造、移动构造、拷贝赋值、移动赋值其中赋值统一为按值传参的 copy-and-swap// 适用于 C17#include algorithm#include cstddef#include iostream#include utilityclass Buffer {public:Buffer() default;explicit Buffer(std::size_t n): size_(n), data_(n ! 0 ? new int[n] : nullptr) {}// 拷贝构造深拷贝Buffer(const Buffer other): size_(other.size_),data_(other.size_ ! 0 ? new int[other.size_] : nullptr) {if (size_ ! 0) {std::copy(other.data_, other.data_ other.size_, data_);}}// 移动构造接管资源源对象置空Buffer(Buffer other) noexcept: size_(other.size_), data_(other.data_) {other.size_ 0;other.data_ nullptr;}// 拷贝赋值与移动赋值合并按值传参Buffer operator(Buffer other) {swap(other); // 交换后other 持有旧资源并在函数结束时析构return *this;}~Buffer() { delete[] data_; }void swap(Buffer other) noexcept {using std::swap;swap(size_, other.size_);swap(data_, other.data_);}std::size_t size() const { return size_; }const int* data() const { return data_; }void set(std::size_t i, int v) {if (i size_) data_[i] v;}private:std::size_t size_ 0;int* data_ nullptr;};// 非成员 swap配合 ADL 使用void swap(Buffer a, Buffer b) noexcept {a.swap(b);}int main() {Buffer a(4);a.set(0, 1);Buffer b(2);b.set(0, 9);a b; // 拷贝赋值b 被拷贝进参数 otherstd::cout a.size() a.data()[0] \n;Buffer c(8);c std::move(a); // 移动赋值a 被移动进参数 otherstd::cout c.size() \n;a a; // 自赋值先拷贝仍然安全std::cout a.size() \n;return 0;}几个实现细节参数按值传递。operator(Buffer other)的other是一个独立副本左值实参触发拷贝构造右值实参触发移动构造。因此一份函数体同时覆盖两种赋值。swap标记noexcept。交换只搬运指针和长度不分配、不抛异常这是 copy-and-swap 能在交换阶段提供强保证的前提。成员swap用using std::swap。对内置类型直接交换若成员是自定义类型ADL 能找到其更高效的swap。非成员swap让标准算法和泛型代码通过 ADL 调用到它。析构函数负责释放。赋值后旧资源由参数other的析构函数释放不需要在operator里手写delete[]。三、为什么它能提供强异常保证强异常保证strong exception guarantee指操作失败时对象保持操作前的状态。copy-and-swap 的执行顺序是构造参数other拷贝或移动。这一步可能抛异常拷贝分配失败。进入函数体swap(*this, other)只交换指针和长度noexcept。函数返回other析构释放原来的资源。如果第 1 步抛异常函数体还没执行*this一个字节都没改自然满足强保证。如果第 2 步不抛已声明noexcept那么整个赋值要么成功要么在修改前失败。第 3 步析构不应抛异常若析构抛出通常直接std::terminate。自赋值也安全a a先把a拷贝给other交换后a拿到副本other拿到旧资源并析构。没有「先 delete 再读自己」的窗口。需要强调强保证成立的前提是拷贝构造成功、swap不抛。如果swap可能抛异常或者析构函数会抛这个论证就不成立。所以swap的noexcept不是装饰而是契约的一部分。四、适用边界与代价copy-and-swap 不是万能的下列情况要谨慎性能敏感且容量可复用。每次赋值都构造一份新副本即使当前缓冲区足够大也要重新分配。对std::vector这类容器标准库的实现通常复用已有容量比 copy-and-swap 更高效。不支持交换的资源。文件句柄、锁、某些平台对象可能没有安全的swap语义或者交换后语义不清晰。多态基类。通过基类指针赋值会切片copy-and-swap 解决不了切片问题多态类型通常应禁止公有赋值或使用虚克隆。移动赋值的性能。按值传参的移动赋值会先移动构造参数再交换通常已经足够快若追求极致可以单独写移动赋值并置空源对象但会增加维护成本。方案异常安全自赋值代码量性能朴素赋值先释放再拷贝弱可能破坏对象需显式检查中可复用容量copy-and-swap强前提swap 不抛天然安全少一份赋值总是先构造副本分别写拷贝/移动赋值可做到强需处理多可控选择建议资源管理类、赋值不频繁、追求实现简洁时用 copy-and-swap容器类或高频赋值场景按需手写并复用容量同时确保异常安全。常见坑点❌operator里先delete旧资源再new新资源。✅ 先构造副本可能抛再swap失败时原对象不变。❌swap没有标记noexcept异常安全论证失效。✅ 让swap只做不抛的指针/值交换并标记noexcept。❌ 用operator(const Buffer)和operator(Buffer)两份实现逻辑不一致。✅ 用按值传参的一份operator(Buffer other)拷贝和移动都由参数构造完成。❌ 认为std::move会移动对象于是写return std::move(*this);。✅std::move只是类型转换按值返回/传参时直接写对象名让编译器选择移动或消除。❌ 移动构造没有把源对象的指针置空导致双重释放。✅ 移动后把源对象的data_设为nullptr、大小设为 0。❌ 只写成员swap泛型代码里swap(a, b)找不到。✅ 在同命名空间提供非成员swap内部调用成员版本。❌ 对多态基类使用按值赋值发生对象切片。✅ 多态类型禁用公有赋值或用clone() 指针/智能指针。❌ 析构函数可能抛异常却指望 copy-and-swap 提供强保证。✅ 析构函数应noexcept清理失败只能记录不能抛出。总结要点说明核心步骤按值构造副本 →noexcept交换 → 副本析构释放旧资源异常保证拷贝失败时原对象不变强保证自赋值天然安全无需显式this other代码复用一份赋值同时服务拷贝与移动前提swap不抛、析构不抛代价每次赋值都构造副本可能多一次分配copy-and-swap 的价值在于把「修改对象」这个危险动作压缩到一次不会失败的交换里。它不是必须遵守的教条理解它依赖的前提swap不抛、析构不抛再根据性能和语义需求决定是否采用才是正确的使用方式。