繁体   English   中英

使用RAII嵌套异常

[英]Using RAII to nest 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块,希望嵌套异常,这至少可以说是丑陋的。

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中,您可以使用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()中的giveErrorContextOnFailure之前添加return

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

还有一个有趣的规则:

不要使用std::uncaught_exception()

Herb Sutter有一篇关于这个主题的好文章 ,完美地解释了这个规则。 简而言之:如果你有一个函数f() ,它在堆栈展开期间从析构函数中调用,就像这样

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

RAII的析RAII看起来像

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM