在C++中调用可能会抛出异常的函数的析构函数

11

我知道在析构函数中不应该抛出异常。

如果我的析构函数调用的函数可能会抛出异常,那么在析构函数中捕获并不将其继续抛出是否可以?或者它仍然可能导致程序中止,因此我根本不应该从析构函数中调用这样的函数?


只是为了澄清,您是在问析构函数捕获异常是否可以,以便它永远不会离开析构函数,还是只要在外部捕获就可以让它离开析构函数? - jalf
我想问一下,如果异常留在析构函数中是否可以。 - Gal Goldman
5个回答

21

是的,这是合法的。一个异常不能从析构函数中“逃逸”,但是在析构函数中或其调用的函数中发生的任何事情都由你决定。

(从技术上讲,异常也可以从析构函数调用中“逃逸”。如果这发生在堆栈展开期间,因为另一个异常被抛出,std::terminate会被调用。因此,按照标准来说这是定义良好的,但这是一个非常糟糕的想法。)


2
抱歉挑剔,但我会使用其他术语而非“合法”的。在析构函数中抛出异常也是“合法”的,即它可以编译和运行。但这是一种不好的做法,会导致不愉快的后果。 - Dima
1
我不确定你希望“legal”是什么意思,但在这一点上我和Dima持相同的观点。尽管C++没有使用“legal”这个词,但它确实定义了一个“良好形式的程序”(可能最接近我所谓的“合法”),“未指定的行为”和“未定义的行为”。在析构函数中允许异常传播可以发生在一个良好形式的程序中,而且这种行为既不是未定义的,也不是未指定的。但这并不能阻止它成为一种几乎普遍不受欢迎的行为。 - CB Bailey
11
在正常情况下(不调用std::terminate()),异常从析构函数中跳出并没有任何问题。只有在另一个异常已经正在传播时(调用std::terminate()),这才是一个问题,因为运行时无法处理两个并行传播的异常概念(或者C++设计者无法想到逻辑上处理这种情况的方式)。 - Martin York
1
这个原则也被称为析构函数中发生的事情,留在析构函数中。 - Gaurav
@SergeRogatch “欺骗?”你的意思是有意撒谎来误导人们吗?你为什么这么想? - jalf
显示剩余7条评论

4

是的。

看一下标准库中的std::fstream类作为示例。

  • close()可能会抛出异常。
  • 析构函数可以调用close(),但析构函数不会抛出异常(它将吞噬任何异常)。

概念是如果析构函数调用任何可能引发异常的方法,则这些方法应该是公共的。因此,如果您对象的用户想要检查异常,他们可以使用公共方法并处理异常。如果他们不关心异常,那就让析构函数处理问题。

回到std::fstream的示例。

{
    std::fstream   text("Plop");
    // Load Text.

    // I don't care if the close fails.
    // So let the destructor handle it and discard exceptions
}



{
    // If this fails to write I should at least warn the user.
    // So in this case I will explicitly try and close it.
    try
    {
        std::ofstram    password("/etc/password");
        // Update the password file.

        password.close();
    }
    catch(...)
    {
          Message.ShowDialog("You failed to update the Password File");
    }
}

2
您可以在这里找到一些例子:https://software.intel.com/sites/products/documentation/doclib/iss/2013/sa-ptr/sa-ptr_win_lin/GUID-D2983B74-74E9-4868-90E0-D65A80F8F69F.htm 如果一个异常在另一个正在传播的异常的堆栈展开期间离开析构函数,则会调用std::terminate()。
当没有堆栈展开进行时,异常可以离开析构函数而不会调用std::terminate()。然而,对于在堆上分配的对象,这将导致内存泄漏,因为"operator delete"不会为抛出其析构函数外的异常的对象调用。令人惊讶的是,在这种情况下,基类的析构函数仍然会被调用:如果派生类的析构函数抛出异常,基类的析构函数会发生什么 如果在析构函数中捕获异常(使异常不离开析构函数),则即使另一个异常的堆栈展开正在进行,也不会有问题。这种情况在这里更深入地描述:http://bin-login.name/ftp/pub/docs/programming_languages/cpp/cffective_cpp/MEC/MI11_FR.HTM

0

简单的回答是,永远不要在析构函数中抛出异常!

复杂的回答是,只有当另一个异常处于活动状态时,异常从析构函数中逃逸才会真正让你陷入困境。这种情况通常发生在你已经从另一个异常中解开堆栈并销毁了相关对象的情况下。在这种情况下,如果异常从析构函数中逃逸,则会调用std::terminate,请注意,您可以通过调用std::set_terminate来设置自己的std::terminate处理程序。默认实现std::terminate是调用abort。

更为复杂的是,大多数希望对其异常安全性做出任何保证的函数,主要是基本保证或强保证,都依赖于底层类型本身在其析构函数中不抛出异常*

真正的问题是,当此错误发生时,您的程序将处于什么状态?您如何恢复?应该在哪里处理此恢复?您需要查看特定情况并解决这些问题。有时捕获异常并忽略它就足够了。其他时候,您需要引起一些警报。

因此,答案是:C++允许在析构函数中抛出异常,但您永远不应该允许它逃逸。

以下是关于异常保证的简要概述(这里有更长的文章

  1. 回顾:简要定义Abrahams异常安全保证(基本、强和nothrow)。

基本保证是,失败的操作可能会改变程序状态,但不会发生泄漏,并且受影响的对象/模块仍然可析构和使用,在一个一致(但不一定可预测)的状态下。

强保证涉及事务提交/回滚语义:失败的操作保证程序状态在所操作对象方面没有改变。这意味着没有影响对象的副作用,包括相关助手对象(如指向正在操作的容器中的迭代器)的有效性或内容。

nothrow保证意味着不会发生失败的操作。该操作不会抛出异常。


据我理解这个问题,他并不是在问关于异常离开析构函数的问题,而只是询问是否可以在析构函数中调用一个会抛出异常的函数,只要析构函数捕获了异常以防止其继续传播就可以。 - jalf

-1

您可能会发现C++ FAQ Lite中的此页面信息丰富。基本答案是“不要这样做”。您计划在哪里捕获此异常?无论您打算在捕获该异常时执行什么操作,都可以通过函数调用或其他方式来完成(例如记录日志或设置标志以警告用户等)。从析构函数中抛出异常会导致整个程序终止的风险。


这与问题有何关联?如果他调用一个可能会抛出std::bad_alloc的函数,那么应该采取什么替代方案?编写一个“包装”函数吗?那不是解决方案,只是将异常隐藏在另一层中。 - MSalters

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接