C++ / a working model

21 / 80   ·   C++11   ·   约 8 分钟

析构函数、异常与 noexcept

先记住这句话

析构负责可靠清理,不适合承担必须报告失败的业务提交。析构函数通常隐式不抛出;异常越过 noexcept 边界会终止程序,即使允许抛出,在栈展开时再次让异常逃出析构也会调用 terminate。

本篇内容
  1. 清理路径不能依赖成功返回
  2. 两个不同的终止条件
  3. 把可失败操作移出析构
  4. 运行示例
  5. 动手练习

清理路径不能依赖成功返回

自动对象在正常离开作用域和异常栈展开时都会析构。析构函数体执行后,成员与基类继续逆序销毁;在函数体里提前 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,让调用者在正常控制流中处理失败,析构只完成不抛出的清理。

继续查证

标准草案链接会随工作草案更新;本文版本标记对应示例最低要求,不表示草案中的所有新规则都适用于旧标准。

回到目录