You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

非栈展开时析构函数抛异常的行为及代码合法性问询

契约式设计库的析构与异常行为问题

定义的out类代码

#include <stdexcept>
#include <type_traits>
#include <utility>

template <typename T, typename Post>
class out {
  public:
    out(T& param, Post check = {})
      : param{param},
        copy{param}, // 要求T可拷贝;可能抛出异常
        check{std::move(check)}
    {}

    ~out() noexcept(false) {
        if (!check(param)) {
            param = std::move(copy); // 回滚;假设此操作永不抛出
            if (!std::uncaught_exceptions()) // 当前无未捕获异常
                throw std::invalid_argument("postcondition violated"); // 仅在无其他异常时抛出
        }
    }

  private:
    T& param;
    T copy;
    Post check;
};

使用示例代码

#include <vector>
#include <cassert>

struct is_not_empty {
    bool operator()(const std::vector<int>& v) { return !v.empty(); }

    // 仅用于演示资源管理的示意代码
    is_not_empty() { /* 获取资源 */ }
    ~is_not_empty() { /* 释放资源 */ }
};

void function_with_postcondition(out<std::vector<int>, is_not_empty> v) {
} // out<>的析构函数在此处抛出异常

int main() {
    std::vector<int> v; // 初始为空
    try {
        function_with_postcondition(v);
        assert(false); // 不会执行到这里
    }
    catch (const std::invalid_argument& e) {
    }
}

核心问题解答

1. function_with_postcondition中的v对象是否能被正确析构?

能。函数正常返回时,v的析构逻辑会完整执行:

  • 先执行析构函数内的后置条件检查、回滚(若需),无未捕获异常时抛出异常;
  • 抛出异常后,栈展开过程中v的所有成员仍会按反向构造顺序被析构,不会跳过任何步骤。

2. out类的Post成员是否能被正确析构?

能。C++规定对象成员的析构顺序与构造顺序相反:out的构造顺序是param(引用无构造)→copy→check,因此析构时会先销毁check,再销毁copy。示例中is_not_empty的析构函数会被正常调用,其获取的资源会被释放。

3. 该示例是否存在资源泄漏?

不存在。out的所有成员都会被正确清理:

  • copy是std::vector<int>,析构时自动释放内存;
  • check是is_not_empty实例,其析构函数会释放资源;
  • param是引用,指向外部对象,无需析构。

4. 是否会递归调用析构函数?

不会。析构函数内仅执行检查、回滚和可能的抛异常操作,这些都不会触发out自身的析构函数。栈展开只会依次析构当前对象的成员,不会递归调用~out()。

5. 上述行为是否属于良定义?

大部分行为是良定义的,但有几个前提:

  • 析构抛异常的逻辑:仅在std::uncaught_exceptions()为0时抛异常,此时无其他未捕获异常,抛出的异常能被外部捕获,属于良定义;若已有未捕获异常则不抛,避免触发std::terminate(),这部分逻辑安全。
  • 回滚操作的安全性:你假设param = std::move(copy)永不抛出,若T的移动赋值可能抛出,且此时无未捕获异常,该异常会离开析构函数,后续再抛invalid_argument会触发std::terminate(),这属于未定义行为,必须保证移动赋值不抛出。
  • Post的调用安全性:若check(param)抛出异常,在无未捕获异常时,该异常会被外部捕获;若已有未捕获异常,会直接触发std::terminate(),因此需确保Post的operator()不会抛出,或额外处理这种情况。

6. 为何不采用assert()、abort()、exit()等其他方式?

  • assert():仅在调试模式生效,发布模式被移除,无法用于生产环境的契约检查;且触发时直接终止程序,不支持回滚和重试。
  • abort()/exit():直接终止程序,无法进行资源清理或回滚,也不允许调用者捕获异常进行重试或恢复。
  • 你的目标是打造异常环境下健壮、支持提交/回滚语义的库,抛出异常是唯一能让调用者捕获并进行后续处理(比如重试)的方式,同时保证栈展开时资源被正确清理。

7. 当异常并非在栈展开阶段离开析构函数时,会发生什么?

当std::uncaught_exceptions()返回0(当前无未捕获异常)时,析构函数抛出的异常会被外部try/catch块捕获:程序会正常执行栈展开,析构当前对象的所有成员,然后进入catch块处理异常,后续程序可继续运行。

只有在栈展开阶段(已有未捕获异常),析构函数抛异常才会触发std::terminate()直接终止程序,你的代码通过std::uncaught_exceptions()判断,已经避免了这种情况。


内容的提问来源于stack exchange,提问作者jdc

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.17 04:42:15