Skip to content

异常安全、栈展开与资源清理

栈展开(Stack Unwinding)

执行 throw 后,控制流离开当前作用域并沿调用栈寻找匹配的 catch。离开每个作用域时,已成功构造的自动存储期对象会按构造的逆序析构:

cpp
void work() {
    File file("input");
    std::vector<int> values;
    values.push_back(1);
    throw std::runtime_error("failed");
} // values 析构 → file 析构 → 向调用方继续找 catch

这就是 RAII 能在异常路径正确释放锁、文件、内存和连接的原因。裸资源会破坏这一保证:

cpp
void unsafe() {
    auto* value = new int(42);
    may_throw();      // 异常时到不了 delete,发生泄漏
    delete value;
}

void safe() {
    auto value = std::make_unique<int>(42);
    may_throw();      // unique_ptr 析构时自动 delete
}

若没有匹配的处理器,异常最终调用 std::terminate();这不意味着所有 RAII 析构都会被跳过,而是程序不能正常恢复执行。

异常安全保证

设计修改操作时,常使用以下层级描述:

保证结果
无抛出保证(no-throw)操作不会抛异常,通常用于 swap、析构、回滚
强保证(strong)操作失败后状态回到操作前,类似事务提交/回滚
基本保证(basic)不泄漏资源,对象保持有效且满足不变量,但状态可改变
无保证异常后状态未知,应避免

“先在副本上完成可能抛异常的工作,再交换提交”是获取强保证的常见方式:

cpp
class ThreadSafeValues {
public:
    void add(int value) {
        std::lock_guard lock(mutex_);
        auto next = values_;       // 复制或扩容可能抛异常,原 values_ 未变
        next.push_back(value);
        values_.swap(next);        // 默认 allocator 下通常为 noexcept 提交
    }

    std::vector<int> snapshot() const {
        std::lock_guard lock(mutex_);
        return values_;            // 返回副本,锁外安全使用
    }

private:
    mutable std::mutex mutex_;
    std::vector<int> values_;
};

锁也必须依赖 RAII:std::lock_guard/std::unique_lock 在异常离开作用域时自动解锁。不要在持锁期间调用不可控的回调或耗时 I/O;它们可能抛异常、重入或造成死锁。

析构函数为何不应抛异常

析构函数通常隐式为 noexcept(true)。异常若从这样的析构函数逃出,即使不是栈展开期间,也会调用 std::terminate();若在已有异常的栈展开过程中再抛出第二个异常,同样无法同时传播两个异常,必须终止。

cpp
class Connection {
public:
    // 显式关闭可报告失败;调用方仍有机会重试、记录或处理。
    void close();

    ~Connection() noexcept {
        try {
            close();
        } catch (...) {
            // 记录到不会抛异常的通道,或执行 best-effort 清理;绝不重新抛出。
        }
    }
};

资源释放可能失败时,优先提供显式 close()/commit()/flush() 让正常流程处理错误;析构函数只做无抛出兜底清理。吞掉异常也不能让数据凭空可靠落盘,因此业务应在析构前显式确认关键操作成功。

std::uncaught_exceptions()

std::uncaught_exception() 是旧的布尔接口,C++17 起弃用;std::uncaught_exceptions() 返回当前正在传播的异常数量。它常用于 scope guard 判断退出路径:

cpp
class TransactionGuard {
public:
    TransactionGuard() : count_(std::uncaught_exceptions()) {}

    ~TransactionGuard() noexcept {
        if (std::uncaught_exceptions() > count_) {
            rollback(); // 正在异常退出
        } else {
            commit();   // 正常退出
        }
    }
private:
    int count_;
    void commit() noexcept;
    void rollback() noexcept;
};

它只用于选择无抛出的清理策略,不能让析构函数抛异常变得安全;也不应把它当作一般业务流程控制工具。

多线程下的异常与对象生命周期

线程入口函数中的异常不能逃出线程函数,否则程序会 std::terminate();必须在线程函数内部捕获,或通过 promise/future、任务系统把异常传回协调者。

更重要的是:mutex 和 destroyed_ 标志不能让“析构期间仍有其他线程调用成员函数”变得安全。 一旦析构开始,对象生命周期正在结束,其他线程继续使用 this 就可能是未定义行为。正确方案是由外部所有权协议保证:先停止接收任务 → 通知退出 → join() 所有工作线程 → 最后销毁对象;或用 shared_ptr/weak_ptr 确保异步任务只在对象仍存活时访问它。

使用 Markdown 与 VitePress 构建