8

人们强烈反对从析构函数中抛出异常。以这个答案为例。我想知道是否std::uncaught_exception()可以用于便携式检测我们是否由于其他一些异常而处于展开堆栈的过程中。

我发现自己故意在析构函数中抛出异常。提到两个可能的用例:

  • 一些涉及刷新缓冲区的资源清理,因此失败可能表示输出被截断。
  • 销毁持有std::exception_ptr可能包含在不同线程中遇到的异常的对象。

简单地忽略这些特殊情况是完全错误的。并且有可能通过抛出异常,一些异常处理程序可能能够提供比析构函数本身写入的更有用的上下文信息std::cerr。此外,为所有失败的断言抛出异常是我的单元测试方法的重要组成部分。在这种情况下,错误消息后跟被忽略的错误条件将不起作用。

所以我的问题是,是否可以抛出异常,除非正在处理另一个异常,还是有理由不这样做?

把它放在代码中:

Foo::~Foo() {
  bool success = trySomeCleanupOperation();
  if (!success) {
    if (std::uncaught_exception())
      std::cerr << "Error in destructor: " << errorCode << std::endl;
    else
      throw FooOperationFailed("Error in destructor", errorCode);
  }
}

据我所知,这应该是安全的,并且在许多情况下比根本不抛出异常要好。但我想验证一下。

4

2 回答 2

10

Herb Sutter 写过关于这个主题的文章:http ://www.gotw.ca/gotw/047.htm

他的结论是永远不要从析构函数中抛出,始终使用在无法抛出的情况下使用的机制报告错误。

两个原因是:

  • 它并不总是有效。有时uncaught_exception返回 true,但可以安全地抛出。
  • 以两种不同的方式报告相同的错误是糟糕的设计,如果用户想知道错误,则必须考虑这两种方式。

请注意,对于任何给定的可重用代码,无法确定它在堆栈展开期间永远不会被调用。无论您的代码做什么,您都无法确定它的某些用户是否不想从具有try/catch适当位置的析构函数调用它来处理其异常。因此,如果可以安全地抛出,则不能依赖uncaught_exception始终返回 true,除非通过记录函数,“不得从析构函数中调用”。如果您采用这种方式,那么所有调用者还必须记录他们的函数,“不得从析构函数中调用”,并且您有一个更烦人的限制。

除此之外,nothrow 保证对用户很有价值——如果他们知道他们所做的特定事情不会抛出,它可以帮助他们编写异常安全的代码。

一种出路是给你的类一个成员函数,如果它失败了,它close会调用并抛出。trySomeCleanupOperation然后析构函数调用trySomeCleanupOperation并记录或抑制错误,但不抛出。然后用户close如果想知道自己的操作是否成功就可以调用,如果不关心就让析构函数处理(包括析构函数被调用作为堆栈展开的一部分的情况,因为之前抛出了异常)接听用户的电话close)。“啊哈!”,你说,“但这违背了 RAII 的目的,因为用户必须记住打电话close!”。是的,有点,但问题不在于 RAII 是否可以做你想做的一切。它不能。问题是它是否始终如一做的比你想要的少(你希望它在trySomeCleanupOperator失败时抛出异常),或者在堆栈展开期间使用时做的不那么令人惊讶。

此外,为所有失败的断言抛出异常是我的单元测试方法的重要组成部分

这可能是一个错误——您的单元测试框架应该能够将 aterminate()视为测试失败。假设在堆栈展开期间断言失败 - 当然你想记录它,但你不能通过抛出异常来做到这一点,所以你把自己画到了一个角落。如果您的断言终止,那么您可以将它们检测为终止。

不幸的是,如果您终止,那么您将无法运行其余的测试(至少,不在该过程中)。但是,如果断言失败,那么一般来说,您的程序处于未知且可能不安全的状态。因此,一旦您的断言失败,您无论如何都不能依赖在该过程中做任何其他事情。您可以考虑将测试框架设计为使用多个进程,或者只是接受足够严重的测试失败将阻止其余测试运行。在测试框架的外部,您可以认为您的测试运行具有三种可能的结果“全部通过,失败,测试崩溃”。如果测试运行未能完成,那么您不会将其视为通过。

于 2013-03-05T11:29:28.093 回答
3

这就是标准对 dtors 和异常的规定:

15.2

(...)

为在从 try 块到引发异常的点的路径上构造的自动对象调用析构函数的过程称为“堆栈展开”。如果在堆栈展开期间调用的析构函数以异常退出,则调用 std::terminate (15.5.1)。[注意:所以析构函数通常应该捕获异常,而不是让它们传播到析构函数之外。——尾注]

由于您提出了非常模棱两可的问题,因此答案取决于:

  • 您可以在 dtor 中抛出异常以使应用程序不会崩溃吗?的,你可以(在条款:它会编译,有时它会正确运行)。
  • 你应该在 dtor 中抛出异常吗?,你不应该,因为它可能(而且通常会)给你带来麻烦。

我想说,需要从 dtor 抛出异常是糟糕设计的标志。看起来,你在你的析构函数中做了一些事情,应该在其他地方做。

于 2013-03-05T11:18:49.270 回答