Appearance
异常安全、栈展开与资源清理
栈展开(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 确保异步任务只在对象仍存活时访问它。