21 / 80 · C++11 · 约 8 分钟
析构函数、异常与 noexcept
析构负责可靠清理,不适合承担必须报告失败的业务提交。析构函数通常隐式不抛出;异常越过 noexcept 边界会终止程序,即使允许抛出,在栈展开时再次让异常逃出析构也会调用 terminate。
清理路径不能依赖成功返回
自动对象在正常离开作用域和异常栈展开时都会析构。析构函数体执行后,成员与基类继续逆序销毁;在函数体里提前 return 并不会跳过这些步骤。这让资源清理可以绑定生命周期,而不必给每条业务返回路径手工补释放代码。
析构未显式写异常说明时,通常是 noexcept(true),但若相关成员或基类的析构允许抛出,隐式异常说明也可能允许抛出。因此“所有析构函数天然 noexcept”不准确;工程上应主动确保资源管理类的析构不会把异常传播出去。
两个不同的终止条件
异常逃出非抛出函数会调用 std::terminate。给析构写 noexcept(false) 只能改变这一项约束,不能解决另一个独立问题:已有异常正在栈展开时,被清理对象的析构若再因异常退出,程序同样终止。
析构内部可以发生异常并在内部捕获,只要它不逃出;所以关键不是是否出现过 throw,而是清理过程能否正常完成。捕获也不能变成无声丢弃重要错误的借口,应事先确定日志、错误状态或显式操作的报告策略。
把可失败操作移出析构
需要确认成功的动作应提供显式 commit()、flush() 或 close(),让调用者在对象仍然存在时处理失败。析构只做不抛出的兜底清理或回滚。示例用整数状态表示事务:提交失败保留原值,离开作用域只增加关闭计数,不尝试在清理阶段再次提交。
这不是把所有错误吞掉,而是把错误交给有恢复上下文的调用者。若底层释放确实可能失败,接口应定义哪些失败可提前报告、哪些只能记录;无法维持资源安全的致命错误则应明确制定终止策略,而不是依赖偶然的异常传播。
容易答错的地方
- 显式 noexcept(false) 不会使栈展开期间抛异常变安全;两个异常同时向外传播仍会终止。
- 析构函数的普通函数体 try/catch 不包围之后自动进行的成员析构;成员也必须有可靠的清理契约。
运行一个例子
最低标准 C++11 · 完整程序 · 下载 .cpp
#include <cassert>
#include <stdexcept>
class Transaction {
int& target_;
int& closed_;
int pending_;
public:
Transaction(int& target, int& closed, int value)
: target_(target), closed_(closed), pending_(value) {}
Transaction(const Transaction&) = delete;
Transaction& operator=(const Transaction&) = delete;
void commit() {
if (pending_ < 0) throw std::invalid_argument("negative value");
target_ = pending_;
}
~Transaction() noexcept { ++closed_; }
};
int main() {
int value = 4;
int closed = 0;
bool caught = false;
try {
Transaction transaction(value, closed, -1);
transaction.commit();
} catch (const std::invalid_argument&) {
caught = true;
}
assert(caught && value == 4 && closed == 1);
{
Transaction transaction(value, closed, 9);
transaction.commit();
}
assert(value == 9 && closed == 2);
}
在本地编译
g++ -std=c++11 -Wall -Wextra -Wpedantic -pthread objects-destructors.cpp -o example && ./example预期结果
预期:正常退出、无输出;所有 assert 通过。
CHECK YOUR UNDERSTANDING
合上答案,试着解释。
为何不能把示例的 commit() 直接移动到析构函数里,并只在 main 外层捕获异常?
查看参考答案
当前析构显式 noexcept,commit 的异常一旦逃出,程序直接 terminate,外层 catch 无法恢复。即使改成 noexcept(false),对象也可能在另一异常的栈展开中销毁,再次抛出仍会终止。保留显式 commit,让调用者在正常控制流中处理失败,析构只完成不抛出的清理。
继续查证
标准草案链接会随工作草案更新;本文版本标记对应示例最低要求,不表示草案中的所有新规则都适用于旧标准。