使用RAII嵌套异常

时间:2013-11-17 21:00:37

标签: c++ exception c++11 raii nested-exceptions

因此,使用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块,希望嵌套异常,这至少可以说是丑陋的。

RAII,Jon Kalb expands作为“责任获取是初始化”,是处理异常而不是使用显式try / catch块的更清晰的方法。使用RAII,显式的try / catch块主要仅用于最终处理异常,例如,以便向用户显示错误消息。

查看上面的代码,在我看来,输入foo()可以被视为有责任将任何异常报告为std::runtime_error("foo failed")并将详细信息嵌套在嵌套异常中。如果我们可以使用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()

2 个答案:

答案 0 :(得分:3)

如果没有在析构函数中捕获(并使用)未捕获的异常的方法,则在没有调用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"));
}

答案 1 :(得分:3)

RAII

是不可能的

考虑简单的规则

  
    

破坏者绝不能扔掉。

  

RAII无法实现您想要的东西。该规则有一个简单的原因:如果析构函数由于飞行中的异常而在堆栈展开期间抛出异常,则调用terminate()并且您的应用程序将死亡。

替代

在C ++ 11中,你可以使用lambdas,它可以让生活更轻松。你可以写

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()中return之前添加giveErrorContextOnFailure

在发布模式下,与try-catch-way相比,通常没有性能面板,因为模板默认是内联的。

还有一条有趣的规则:

  
    

请勿使用std::uncaught_exception()

  

Herb Sutter有一个很好的article about this话题,完美地解释了这条规则。简而言之:如果你有一个函数f()在堆栈展开期间从析构函数中调用

看起来像这样

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

RAII的析构函数看起来像

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

然后将始终使用析构函数中的第一个分支,因为在外部析构函数中,堆栈展开std::uncaught_exception()将始终返回true,即使在包含析构函数RAII的析构函数中调用的函数内也是如此。