在构造函数中抛出异常后,析构函数被调用

73

我曾经认为,在C++中,如果构造函数抛出异常,则不会调用此“部分构造”的类的析构函数。

但在C++11中似乎不再是这样了:我使用g++编译了以下代码,并将"X destructor"打印到控制台。为什么会这样呢?

#include <exception>
#include <iostream>
#include <stdexcept>
using namespace std;

class X
{
public:
    X() : X(10)
    {
        throw runtime_error("Exception thrown in X::X()");    
    }
    X(int a)
    {
        cout << "X::X(" << a << ")" << endl;
    }
    ~X()
    {
        cout << "X destructor" << endl;
    }
};

int main()
{
    try
    {
        X x;
    }
    catch(const exception& e)
    {
        cerr << "*** ERROR: " << e.what() << endl;
    }
}

输出

Standard out:
X::X(10) 
X destructor
Standard error: 
*** ERROR: Exception thrown in X::X()

这里有一个liveworkspace链接,展示了输出结果。 - Sam Miller
2个回答

82

委托构造函数确实是一项新功能,它引入了新的销毁逻辑。

让我们重新审视对象的生命周期: 对象的生命周期始于某个构造函数完成时。(参见15.2/2。标准称之为“主要构造函数”。) 在您的情况下,这是构造函数X(int)。第二个委托构造函数X()现在只是一个普通成员函数。在作用域取消时,将调用所有完全构造对象的析构函数,其中包括x

这的影响实际上相当深远: 您现在可以将“复杂”的工作负载放入构造函数中,并充分利用通常的异常传播,只要您使构造函数委托给另一个构造函数即可。这样的设计可以避免需要各种“init”函数,这些函数曾经在不希望将太多工作放入常规构造函数中时非常流行。

定义您所看到行为的特定语言如下:

  

[C++11: 15.2/2]: [..] 同样,如果对象的非委托构造函数完成执行,并且该对象的委托构造函数以异常退出,则将调用对象的销毁函数。 [..]


2
@Nawaz: "基本概念 - 对象生命周期"? - Kerrek SB
@KerrekSB: 添加了 Lightness_Races_in_Orbit 已删除答案的引用。;-) - Nawaz

26

我曾经认为,在C++中,如果构造函数抛出异常,则这个“部分构造”的类的析构函数不会被调用。

但是在C++11中似乎不再是这样了。

仍然是真的。自C++03以来没有改变(对于某些值得注意的事情;-))

您所思考的还是正确的,但是当异常被抛出时,不存在部分构造的对象

C++03 TC1标准说(重点是我的):

一个部分构造或者部分销毁的对象将会有所有完全构造的子对象的析构函数执行,也就是为那些构造函数已经执行完成且析构函数还没有开始执行的子对象。

即,任何已经完成构造的对象都会通过执行其析构函数来被销毁。这是一个简洁明了的规则。

在C++11中也基本上适用同样的规则:一旦X(int)返回,对象的“构造函数已经执行完成”,因此它是完全构造的,因此其析构函数将在适当的时间(例如在其超出作用域范围时或在其构造过程的某个后续阶段抛出异常时)运行。本质上来说,仍然是相同的规则。

委托构造函数的主体在其他构造函数之后运行并可以执行额外的工作,但这并不改变对象的构造已经完成的事实,因此它是完全构造的。委托构造函数类似于派生类的构造函数,在基类的构造函数完成后执行更多的代码。从某种意义上讲,您可以将您的示例看作是这样的:

class X
{
public:
    X(int a)
    {
        cout << "X::X(" << a << ")" << endl;
    }
    ~X()
    {
        cout << "X destructor" << endl;
    }
};
    
class X_delegating : X
{
public:
    X_delegating() : X(10)
    {
        throw runtime_error("Exception thrown in X::X()");    
    }
};

实际上不是这样的,只有一种类型,但它在很大程度上类似,因为X(int)构造函数运行,然后在委托构造函数中运行其他代码,如果出现异常,则会销毁X“基类”(实际上并不是基类)。


3
这个答案只得到了一个赞,真是奇怪。虽然Kerrek提到了这个功能存在的原因,但我认为这个答案更好地回答了实际问题。 - user948581
@Tibo,是的,我也这么认为;-) 不过我来晚了,当我写我的答案时,我想Kerrek的答案已经被接受了。 - Jonathan Wakely

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