使用RAII嵌套异常

Using RAII to nest exceptions

本文关键字:异常 嵌套 RAII 使用      更新时间:2023-10-16

因此,使用std::nested_exception在C++中嵌套异常的方法是:

void foo() {
  try {
    // code that might throw
    std::ifstream file("nonexistent.file");
    file.exceptions(std::ios_base::failbit);
  }
  catch(...) {
    std::throw_with_nested(std::runtime_error("foo failed"));
  }
}

但这种技术在每个级别都使用显式的try/catch块来嵌套异常,这至少可以说是丑陋的。

Jon Kalb将RAII扩展为"责任获取就是初始化",它是一种更干净的处理异常的方法,而不是使用显式的try/catch块。对于RAII,显式try/catch块在很大程度上只用于最终处理异常,例如向用户显示错误消息。

查看上面的代码,在我看来,输入foo()可以被视为意味着有责任将任何异常报告为std::runtime_error("foo failed"),并将详细信息嵌套在nested_exception中。如果我们可以使用RAII来获得这一责任,代码看起来会干净得多:

void foo() {
  Throw_with_nested on_error("foo failed");
  // code that might throw
  std::ifstream file("nonexistent.file");
  file.exceptions(std::ios_base::failbit);
}

有没有办法在这里使用RAII语法来替换显式try/catch块?


要做到这一点,我们需要一个类型,当调用其析构函数时,检查析构函数调用是否是由于异常引起的,如果是,嵌套该异常,并抛出新的嵌套异常,以便正常继续展开。这可能看起来像:

struct Throw_with_nested {
  const char *msg;
  Throw_with_nested(const char *error_message) : msg(error_message) {}
  ~Throw_with_nested() {
    if (std::uncaught_exception()) {
      std::throw_with_nested(std::runtime_error(msg));
    }
  }
};

然而,std::throw_with_nested()要求"当前处理的异常"处于活动状态,这意味着它除了在catch块的上下文中之外不起作用。所以我们需要这样的东西:

  ~Throw_with_nested() {
    if (std::uncaught_exception()) {
      try {
        rethrow_uncaught_exception();
      }
      catch(...) {
        std::throw_with_nested(std::runtime_error(msg));
      }
    }
  }

不幸的是,据我所知,在C++中定义的rethrow_uncaught_excpetion()是独一无二的。

在析构函数中没有捕获(和使用)未捕获异常的方法的情况下,如果不调用std::terminate(在异常处理上下文中抛出异常时),就无法在析构构函数的上下文中重新抛出嵌套或未嵌套的异常。

std::current_exception(与std::rethrow_exception组合)将只返回指向当前处理的异常的指针。这排除了它在该场景中的使用,因为本例中的异常是明确未处理的。

鉴于以上情况,唯一的答案是从美学的角度给出的。功能级别的try块使其看起来不那么难看。(根据您的风格偏好进行调整):

void foo() try {
  // code that might throw
  std::ifstream file("nonexistent.file");
  file.exceptions(std::ios_base::failbit);
}
catch(...) {
  std::throw_with_nested(std::runtime_error("foo failed"));
}

使用RAII是不可能的

考虑的简单规则

析构函数决不能抛出

RAII不可能实现您想要的东西。该规则有一个简单的原因:如果析构函数在堆栈展开过程中由于飞行中的异常而抛出异常,则调用terminate(),您的应用程序将失效。

另一种选择

在C++11中,你可以使用Lambda,这可以让生活变得更轻松。你可以写

void foo()
{
    giveErrorContextOnFailure( "foo failed", [&]
    {
        // code that might throw
        std::ifstream file("nonexistent.file");
        file.exceptions(std::ios_base::failbit);
    } );
}

如果您以以下方式实现函数giveErrorContextOnFailure

template <typename F>
auto giveErrorContextOnFailure( const char * msg, F && f ) -> decltype(f())
{
    try { return f(); }
    catch { std::throw_with_nested(std::runtime_error(msg)); }
}

这有几个优点:

  • 您封装了错误的嵌套方式
  • 如果在整个程序范围内严格遵循此技术,则可以对整个程序更改嵌套错误的方式
  • 错误消息可以像RAII中那样写在代码之前。这种技术也可以用于嵌套作用域
  • 代码重复较少:不必编写trycatchstd::throw_with_nestedstd::runtime_error。这使代码更易于维护。如果你想改变程序的行为,你只需要在一个地方改变你的代码
  • 返回类型将自动推导出来。因此,如果函数foo()应该返回一些东西,那么只需在函数foo()中的giveErrorContextOnFailure之前添加return

在发布模式中,与try-catch方式相比,通常不会有性能面板,因为默认情况下模板是内联的。

还有一条有趣的规则:

请勿使用std::uncaught_exception()

赫伯·萨特有一篇关于这个话题的好文章,完美地解释了这个规则。简而言之:如果你有一个函数f(),它在堆栈展开过程中从析构函数内被调用,看起来像这个

void f()
{
    RAII r;
    bla();
}

其中RAII的析构函数看起来像

RAII::~RAII()
{
    if ( std::uncaught_exception() )
    {
        // ...
    }
    else
    {
        // ...
    }
}

则析构函数中的第一个分支将始终被采用,因为在堆栈展开期间的外部析构函数std::uncaught_exception()将始终返回true,即使在从该析构函数调用的函数内部,包括RAII的析构函数。