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

C++ using RAII with destructor that throws

本文关键字:析构函数 使用 RAII C++      更新时间:2023-10-16

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

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

因此,首先,将析构函数标记为抛出(noexcept(false))。接下来,跟踪ctor中活动异常的数量,将其与dtor中的值进行比较:如果dtor中有更多未捕获的异常,则您知道当前正在进行堆栈展开,再次抛出将导致对std::terminate的调用。

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

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

ScopeGuard文章中的建议是

在异常领域中,如果您的"撤消/恢复"操作失败,则根本无法执行任何操作。您尝试执行撤消操作,然后继续执行,不管撤消操作是否成功。

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

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

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

当抛出异常时,您真正知道的只是程序处于无效状态。有些不可能的事情最终变成了可能,你不应该感到惊讶。就我个人而言,我看到的Alexandrescu的建议比其他情况更有意义的情况要多得多:尝试清理,但要认识到第一个异常意味着事情已经处于无效状态,所以额外的故障——尤其是由第一个问题("错误级联")引起的故障——应该不会令人惊讶。尝试处理它们不会有好的结果。


我可能应该提到,Proto船长正是按照你的建议行事:

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

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