C++ 使用 RAII 和抛出的析构函数

C++ using RAII with destructor that throws

假设我有 RAII class:

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 对象始终是堆栈分配的对象 - 这不是一般的析构函数抛出问题。

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

因此,首先,将析构函数标记为抛出 (noexcept(false))。接下来跟踪ctor中活跃异常的数量,与dtor中的值进行比较:如果dtor中未捕获的异常多,就知道当前正处于栈展开的过程中,再次抛出会导致调用至 std::terminate.

现在你决定你到底有多优秀以及你希望如何处理这种情况:终止程序,或者只是吞下内部异常?

如果uncaught_exception(单数)returns为真,一个糟糕的模仿是不抛出,但这使得异常在从试图捕获和展开的展开触发的不同dtor调用时不起作用处理 你的 异常。此选项在当前 C++ 标准中可用。

the ScopeGuard article 的建议是

In the realm of exceptions, it is fundamental that you can do nothing if your "undo/recover" action fails. You attempt an undo operation, and you move on regardless whether the undo operation succeeds or not.

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

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

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

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


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

When Cap’n Proto code might throw an exception from a destructor, it first checks std::uncaught_exception() to ensure that this is safe. If another exception is already active, the new exception is assumed to be a side-effect of the main exception, and is either silently swallowed or reported on a side channel.

但是,正如 Yakk 所说,在 C++11 中,析构函数默认变为 nothrow(true)。这意味着如果你想这样做,你需要确保在 C++11 和更高版本中你将析构函数标记为 nothrow(false)。否则,即使没有其他异常在运行中,从析构函数中抛出异常也会终止程序。请注意,"If another exception is already active, the new exception is assumed to be a side-effect of the main exception, and is either silently swallowed or reported on a side channel."