如果没有uncaught_exception,则在C++析构函数中有例外

MvG*_*MvG 8 c++ destructor exception

人们强烈反对从析构函数中抛出异常.以这个答案作为一个例子.我想知道是否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);
  }
}
Run Code Online (Sandbox Code Playgroud)

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

Ste*_*sop 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失败时抛出异常),或者在堆栈展开期间使用时不那么令人惊讶.

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

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

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