第二十三章 异常安全与 RAII 深入

← 返回目录

本章目标:理解异常安全的四个等级、RAII 的深层机制、 强异常保证、copy-and-swap 惯用法、noexcept 的正确用法。 这是"库作者级别"的品质要求,普通代码也能受益。

23.1 为什么异常安全是硬需求

异常可能在任何一行抛出(内存不足、IO 错误、用户代码)。 如果函数中途抛异常:

  1. 栈展开:所有栈上对象按构造逆序析构(RAII 保证资源释放)
  2. 但**部分完成的状态修改不会回滚**!
void transfer(BankAccount& a, BankAccount& b, double amt) {
    a.withdraw(amt);        // 先扣钱
    b.deposit(amt);         // 如果这里抛异常 → 钱丢了!
}

这就是"异常不安全":抛异常后数据处于不一致状态。 银行的代码绝对不能这样写。

23.2 异常安全的四级承诺(重要概念)

从弱到强,函数应该尽量做更强的承诺:

  1. 基本保证(Basic Guarantee)

抛异常时:资源不泄漏,对象状态**合法但可能被部分修改** —— 大多数函数都在这个级别

  1. 强保证(Strong Guarantee)

抛异常时:**要么全部成功,要么完全没发生**(事务语义)

  1. 无抛出(No-throw Guarantee)

绝不抛异常(noexcept 声明)——析构、移动操作必须

  1. 无失败(No-fail)——理论极限,几乎不存在

人话:写函数前问自己——"它抛异常时,世界的状态是什么?"

23.3 达成强保证的手段:先验证,再修改

把"可能失败的操作"全部前置,成功后一次性提交:

void transfer(BankAccount& a, BankAccount& b, double amt) {
    a.withdraw(amt);        // 第一处修改:可能失败?
    b.deposit(amt);         // 第二处修改:可能失败!
}

改为:

void transfer_safe(BankAccount& a, BankAccount& b, double amt) {
    if (a.balance() < amt)
        throw std::runtime_error("余额不足");   // 先验证
    if (!b.can_accept(amt))
        throw std::runtime_error("接收失败");   // 全部验证完
    // 两处修改都不会失败(noexcept 操作)→ 强保证
    a.withdraw(amt);
    b.deposit(amt);
}

"先验证后提交" = 事务思维。

23.4 copy-and-swap 惯用法(强保证的经典实现)

给类写"operator="时,最优雅的强保证写法:

class Widget {
    std::vector<int> data_;
public:
    Widget() = default;
    Widget(const Widget&) = default;         // 拷贝构造

    Widget& operator=(Widget other) {        // 按值传参!
        swap(other);                         // 与临时副本交换
        return *this;
    }   // other 出作用域销毁旧数据(自动释放)
    void swap(Widget& o) noexcept {
        data_.swap(o.data_);
    }
};

原理:

  1. 按值传参:拷贝在进入函数前完成——失败则在进入前就抛,

原对象纹丝不动(强保证)

  1. 交换是 noexcept:绝不失败
  2. 函数结束:临时对象销毁,释放旧资源

一条规则:operator= 用 copy-and-swap,自动获得强保证 + 异常安全。

23.5 RAII 深入:为什么它是 C++ 的基石

RAII 本质:把"资源生命周期"绑定到"对象生命周期"。 资源包括:内存、文件、锁、句柄、网络连接、数据库事务。

// 锁(已见过)
std::lock_guard<std::mutex> l(mtx);
// 文件
std::ofstream f("x.txt");
// 智能指针
std::unique_ptr<T> p = std::make_unique<T>();
// 事务(自写)
class Transaction {
public:
    explicit Transaction(Db& db) : db_(db) { db_.begin(); }
    ~Transaction() {
        if (committed_) db_.commit();
        else db_.rollback();
    }
    void commit() { committed_ = true; }
private:
    Db& db_;
    bool committed_ = false;
};

void do_work(Db& db) {
    Transaction tx(db);       // 构造 = 开始事务
    db.insert(...);
    // 忘记 commit()?析构自动回滚!异常?同样回滚!
}

自己写 RAII 类的原则:

  1. 构造函数拿到资源(可能失败 → 构造函数里抛异常,安全)
  2. 析构函数释放资源(绝不抛异常)
  3. 禁止拷贝(或实现正确拷贝),支持移动

23.6 为什么析构函数不能抛异常

栈展开期间如果析构函数再抛异常:

~MyClass() {           // 隐含 noexcept
    cleanup();         // 如果 cleanup 抛异常 → terminate
}

处理:析构里捕获所有异常,吞掉或记录:

~MyClass() {
    try { cleanup(); }
    catch (...) { /* 记录日志,但绝不再抛出 */ }
}

23.7 noexcept 的正确用法

标记 noexcept 的函数:

  1. 可以优化(不需要维护异常栈状态)
  2. 一旦真抛 → 直接 terminate(快速失败,不静默)

该标记的:

不该标记的:

void f() noexcept { ... }     // 内部调用了会抛异常的函数
// 一旦真抛 → terminate,而不是向上传播!

经验:宁可少标,不可错标。

23.8 异常与性能的真相

  1. 现代实现(Itanium ABI):异常**不抛出时零成本**

(代码里没有 try/catch 时完全不引入额外开销)

  1. 抛出时有成本:构建异常对象 + 栈展开 + 查处理表

大约微秒级——每秒几千次是性能问题,每秒几次无所谓

  1. 老说法"异常慢 10 倍"来自 1990 年代的实现,早过时

结论:正常业务代码用异常没问题; 实时/极高性能路径用 expected(零异常路径开销更稳)。

23.9 实战:强保证的容器类

写一个支持强保证的栈容器(完整版见测试):

template <typename T>
class SafeStack {
    std::vector<T> data_;
public:
    void push(const T& v) {
        data_.push_back(v);      // 若失败:vector 自身保持原状态
    }                            // (vector 提供强保证)
    void pop() {
        if (data_.empty()) throw std::runtime_error("空栈");
        data_.pop_back();
    }
    // 移动构造 noexcept
    SafeStack(SafeStack&& o) noexcept = default;
};

标准库容器的保证:

23.10 陷阱清单

  1. 析构函数里抛异常 → terminate
  2. 构造函数里抛出 → 已构造的成员正常析构,但对象不存在

(资源放成员里(RAII)就安全;放裸指针里就泄漏!)

  1. 忘 lock_guard 手动 unlock → 异常路径死锁
  2. 移动操作标了 noexcept 却没实现好 → terminate
  3. new T 和 delete 配对(永远用智能指针)
  4. 假设"这行不会抛异常"却抛了 → 状态损坏
  5. 捕获异常后吞掉不处理(至少记录日志)

本章小结

练习题(配套测试:测试_第23章_异常安全.cpp)


  1. 实现带 copy-and-swap 的类并验证强保证。
  2. 写一个 RAII Transaction 类模拟回滚。
  3. 写一个 SafeStack 并测试异常路径。
  4. 验证:vector::push_back 失败时原 vector 不变。
  5. 列出自己代码里违反基本保证的地方并修复。