C++异常安全设计原则:如何写出不崩溃的系统级代码【Core Guidelines推荐】

不崩溃的系统级C++代码关键在于异常发生时资源不泄漏、对象可析构、状态可回退;必须遵循RAII、析构函数声明noexcept、赋值采用copy-and-swap,且每种资源需专属noexcept管理类。

写出不崩溃的系统级 C++ 代码,关键不是“避免抛异常”,而是让每次异常发生时——资源不泄漏、对象可析构、状态可回退。Core Guidelines 明确要求:所有资源管理必须基于 RAII,所有析构函数必须 noexcept,所有强保证操作优先用 copy-and-swap

为什么析构函数必须加 noexcept

栈展开(stack unwinding)期间若析构函数意外抛出异常,C++ 标准强制调用 std::terminate() —— 程序直接终止,毫无回旋余地。这不是理论风险,而是真实 crash 的常见源头。

  • 所有自定义析构函数应显式声明为 ~MyClass() noexcept,哪怕它只调用 fclose()delete
  • 切勿在析构函数中调用可能抛异常的函数(如 std::ofstream::close() 可能抛 std::ios_base::failure;改用 std::ofstream::close() + 检查 failbit
  • 标准库容器、智能指针、std::lock_guard 的析构函数均已标记 noexcept,可放心依赖

operator= 怎么写才真正强异常安全?

裸写赋值运算符极易破坏强保证:比如先 delete[] datanew,中间抛异常就导致悬空指针或内存泄漏。Core Guidelines 推荐唯一可靠模式:copy-and-swap

class Buffer {
    std::unique_ptr data_;
    size_t size_;

public:
    Buffer& operator=(Buffer other) noexcept {  // 注意:参数传值,自动拷贝
        swa

p(*this, other); // swap 是 noexcept 的 return *this; } friend void swap(Buffer& a, Buffer& b) noexcept { using std::swap; swap(a.data_, b.data_); swap(a.size_, b.size_); } };
  • 参数用值传递(Buffer other),触发拷贝构造——失败则原对象完全不受影响
  • swap 必须是 noexcept,且只交换指针/整数等基础成员,不涉及内存分配
  • 无需手动释放旧资源,旧资源随临时对象 other 在函数退出时自动析构

RAII 不是“用智能指针”就完事,而是“每种资源都得有专属类”

很多团队误以为用了 std::unique_ptr 就算 RAII 完成,结果遇到文件句柄、POSIX 信号量、GPU buffer、自定义锁等非内存资源时仍泄漏。Core Guidelines 强调:RAII 是接口契约,不是语法糖。

  • 对每个低层资源(如 int fdpthread_mutex_t*),必须封装为独立类,构造获取、析构释放
  • 禁用拷贝(= delete),移动构造/赋值必须 noexcept
  • 不要把多种资源塞进一个类里——例如同时管理文件和网络 socket 的类,违反“单一职责”,增加异常路径复杂度
  • 示例:文件句柄类应像 std::fstream 那样,在析构时静默关闭,不抛异常

最常被忽略的一点:异常安全不是靠“catch 住所有异常”来实现的,而是靠资源绑定到作用域、状态变更延后到无异常操作、以及严格限制 noexcept 边界。一旦在析构或 swap 中出现未声明的异常,系统级稳定性就已实质崩塌——而这种问题往往在压力测试或日志关闭时才暴露。