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

gsf*_*gsf 6 c++ exception raii

假设我有RAII课程:

class Raii {
    Raii() {};
    ~Raii() { 
        if (<something>) throw std::exception();
    }
};
Run Code Online (Sandbox Code Playgroud)

如果我有这个功能:

void foo() {
    Raii raii;    

    if (something) {
       throw std::exception();
    }
} 
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

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

Yak*_*ont 7

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

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

现在你确切地确定了你的真实性以及你希望如何处理这种情况:终止程序,或者只是吞下内部异常?

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


Max*_*ert 5

ScopeGuard 文章的建议是

\n\n
\n

在例外情况下,如果“撤消/恢复”操作失败,您将无能为力,这一点至关重要。您尝试撤消操作,然后无论撤消操作是否成功,您都会继续。

\n
\n\n

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

\n\n
    \n
  1. 我设法耗尽内存并得到std::bad_alloc异常
  2. \n
  3. 我的清理代码记录了错误
  4. \n
  5. 不幸的是,写入失败(可能磁盘已满),并尝试抛出异常
  6. \n
\n\n

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

\n\n

当抛出异常时,您真正知道的是程序处于无效状态。有些不可能的事情最终变成可能,你不应该感到惊讶。就我个人而言,我见过更多 Alexandrescu 的建议比其他建议更有意义的情况:尝试清理,但认识到第一个异常意味着事物已经处于无效状态,因此额外的失败 - 特别是由第一个问题(“错误级联”)引起的失败——不应感到意外。尝试处理它们不会有好结果。

\n\n
\n\n

我可能应该提到 Cap\'n Proto 完全符合您的建议:

\n\n
\n

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

\n
\n\n

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

\n