C++ 赋值运算符的异常安全(Copy-and-Swap)是什么?(如何编写健壮的代码)

2次阅读

copy-and-swap能解决赋值异常安全问题,因其将构造新状态与替换旧状态分离:先在临时对象中完成可能失败的操作,再以noexcept swap原子切换,确保原对象要么全更新、要么完全不变。

C++ 赋值运算符的异常安全(Copy-and-Swap)是什么?(如何编写健壮的代码)

为什么 copy-and-swap 能解决赋值异常安全问题

因为默认的赋值运算符在中途抛异常时,可能留下“半更新”对象:原对象被部分修改、资源泄漏、或处于不一致状态。而 copy-and-swap 把“构造新状态”和“替换旧状态”彻底分离——先在临时对象里安全完成所有可能失败的操作(如内存分配、拷贝),成功后再用无异常的 swap 原子切换,原对象要么全换、要么完全不变。

关键前提是:你的 swap 函数必须是 noexcept 的,且不能抛异常;拷贝构造函数可以抛异常,但那只会让临时对象构造失败,不影响原对象。

operator= 实现中哪些地方最容易写错

常见错误不是逻辑错,而是破坏了异常安全契约:

  • 漏掉自赋值检查?其实 copy-and-swap 本身天然支持自赋值(swap(x, x) 是合法且无害的),所以不用特判——加了反而多余
  • swap 写成成员函数但没声明为 noexcept,导致编译器不敢优化、甚至触发未定义行为(比如在 std::vector 重分配时调用它)
  • swap 里手动交换裸指针却忘了置空原指针,造成二次释放;或者交换 std::unique_ptr 时用了 .release() 而不是直接赋值
  • 拷贝构造函数里做了深拷贝但没处理分配失败(比如没用 new (std::nothrow) 或没捕获 std::bad_alloc),导致异常从构造函数逃逸——这会让 copy-and-swap 的第一步就崩,但至少不伤原对象

什么时候不该用 copy-and-swap

它不是银弹。性能敏感路径或资源极重的对象要谨慎:

立即学习C++免费学习笔记(深入)”;

  • 每次赋值都触发一次完整拷贝 + 一次 swap,对大对象(如含几 MB 缓冲区的类)开销明显;若确定赋值频繁且异常极少,手写“强异常安全”的赋值(先释放再拷贝再重建)可能更优
  • 类没有合适的 swap 实现,或 swap 本身依赖外部锁/系统调用(无法保证 noexcept
  • 移动语义已足够——c++11 后,优先实现 operator=(T&&) 移动赋值,它比 copy-and-swap 更轻量;而 copy-and-swap 主要保底应对左值赋值

示例中常见的误用:operator=(const T& other) { T temp(other); swap(*this, temp); } —— 这里 swap 必须是非成员函数(ADL 可见),且最好用 using std::swap; + swap(...) 调用,避免意外调用到不安全的默认版本。

如何验证你的 operator= 真的异常安全

不能只靠“没崩过”;得主动注入故障点:

  • 在拷贝构造函数里某次 new 后手动 throw std::bad_alloc(),观察原对象是否保持原样(可用断言检查内部指针、计数器、文件描述符等)
  • std::set_new_handler 模拟内存耗尽,或用 __gnu_cxx::throw_allocator(GCC)测试容器内部分配失败
  • 检查编译器警告:Clang/GCC 对未标记 noexceptswap 在异常上下文中会提示 “call to function ‘swap’ that is potentially throwing”
  • 静态分析工具如 clang++ -fsanitize=undefinedAddressSanitizer 能抓到 swap 后原对象被误用的问题

最常被忽略的一点:异常安全 ≠ 不抛异常,而是“失败时不留垃圾”。哪怕你所有函数都加了 noexcept,只要 swap 里有未检查的 close(fd) 失败,就可能让文件句柄泄露——这种错误不会 crash,但会在长期运行中悄悄拖垮系统。

text=ZqhQzanResources