12

所以在 C++ 中嵌套异常的方法std::nested_exception是:

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 将其扩展为“责任获取就是初始化”,是一种处理异常的更简洁的方法,而不是使用显式的 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));
      }
    }
  }

不幸的是,据我所知,rethrow_uncaught_excpetion()在 C++ 中没有任何定义。

4

2 回答 2

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"));
}
于 2013-11-17T23:45:56.783 回答
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 中一样。这种技术也可以用于嵌套范围。
  • 代码重复更少:您不必编写trycatch和. 这使您的代码更易于维护。如果你想改变你的程序的行为,你只需要在一个地方改变你的代码。std::throw_with_nestedstd::runtime_error
  • 返回类型会自动推导出来。因此,如果您的函数foo()应该返回一些东西,那么您只需在函数 foo() 中添加return之前giveErrorContextOnFailure

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

另一个有趣的规则是:

不要使用std::uncaught_exception().

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

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

的析构函数在RAII哪里

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

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

于 2013-11-18T10:31:57.650 回答