【问题标题】:C++ using RAII with destructor that throwsC++ 使用带有析构函数的 RAII 抛出
【发布时间】:2016-07-21 22:52:55
【问题描述】:

假设我有 RAII 类:

class Raii {
    Raii() {};
    ~Raii() { 
        if (<something>) throw std::exception();
    }
};

如果我有这个功能:

void foo() {
    Raii raii;    

    if (something) {
       throw std::exception();
    }
} 

这很糟糕,因为在清理第一个异常时,我们可以再次抛出,这将终止进程。

我的问题是 - 将 raii 用于清理可能抛出的代码的好模式是什么?

例如这是好是坏 - 为什么?

class Raii {
    Raii() {};
    ~Raii() {
        try {
           if (<something>) throw std::exception();
        }
        catch (...) {
           if (!std::uncaught_exception())
               throw;
        }
    }
};

请注意,Raii 对象始终是堆栈分配的对象 - 这不是析构函数问题的一般抛出。

【问题讨论】:

  • 这与“如果我有一个析构函数可以抛出的类,我如何防止终止”的更一般的情况并没有什么不同。我认为除了“析构函数应该抛出”之外没有什么好的答案。
  • 没有析构函数做一些工作就没有 RAII。我真的希望有一些事情可以做。根据 std::uncaught_exception 说点什么,例如,您可以在其中检测是否有异常在进行中。

标签: c++ exception raii


【解决方案1】:

C++ 几乎肯定会有一个函数来获取 C++1z 的当前异常计数(如果他们按时发布,也就是 C++17!):std::uncaught_exceptions(注意复数“s”)。此外,析构函数默认声明为noexcept(这意味着如果您尝试通过异常退出析构函数,则会调用std::terminate)。

所以,首先,将你的析构函数标记为 throwing (noexcept(false))。接下来跟踪ctor中活动异常的个数,与dtor中的值进行比较:如果dtor中有更多未捕获的异常,就知道当前正在堆栈展开过程中,再次抛出会导致调用到std::terminate

现在你决定了你到底有多出色以及你希望如何处理这种情况:终止程序,还是吞下内部的异常?

如果uncaught_exception(单数)返回 true,则不抛出异常,但是当从试图捕获和处理你的的展开触发的不同 dtor 调用时,这会导致异常不起作用> 例外。此选项在当前的 C++ 标准中可用。

【讨论】:

  • @kerrek C++17 已经完成了吗?
  • 相关论文已被投票和应用(N4582),几乎不可能被删除,因为 WD 被认为是 17 年的“功能完整”。
【解决方案2】:

the ScopeGuard article 的建议是

在异常领域中,如果您的“撤消/恢复”操作失败,您将无能为力。您尝试撤消操作,无论撤消操作是否成功,您都会继续前进。

这听起来可能很疯狂,但请考虑一下:

  1. 我设法用完内存并得到一个std::bad_alloc 异常
  2. 我的清理代码记录了错误
  3. 很遗憾,写入失败(可能磁盘已满),并尝试抛出异常

我可以撤消日志写入吗?我应该试试吗?

当抛出异常时,你真正知道的只是程序处于无效状态。你不应该对一些不可能的事情最终变成可能感到惊讶。就我个人而言,我见过更多的案例,其中 Alexandrescu 的建议比其他方法更有意义:尝试清理,但要认识到第一个异常意味着事情已经处于无效状态,因此会出现额外的故障——尤其是由第一个问题(“错误级联”)——应该不足为奇。并且试图处理它们不会有好的结果。


我应该提一下,Cap'n Proto 完全按照您的建议行事:

当 Cap'n Proto 代码可能从析构函数中抛出异常时,它首先检查 std::uncaught_exception() 以确保这是安全的。如果另一个异常已经处于活动状态,则假定新异常是主要异常的副作用,并且会被静默吞下或报告到侧通道。

但是,正如 Yakk 所说,析构函数在 C++11 中默认变为 nothrow(true)。这意味着如果你想这样做,你需要确保在 C++11 和更高版本中你将析构函数标记为nothrow(false)。否则,即使没有其他异常在运行,从析构函数中抛出异常也会终止程序。请注意,“如果另一个异常已经处于活动状态,则假定新异常是主要异常的副作用,并且要么被静默吞下,要么在旁通道上报告。”

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2014-07-06
    • 2020-06-13
    • 1970-01-01
    • 2014-07-15
    • 2022-01-16
    • 2017-12-22
    • 2015-08-26
    相关资源
    最近更新 更多