如果构造函数抛出异常会发生什么?

36

那么我们会得到UB吗?

我尝试了这个:

#include <iostream>

struct B
{
    B(){ std::cout << "B()" << std::endl; }
    ~B(){ std::cout << "~B()" << std::endl; }
};

struct A
{
    B b;
    A(){ std::cout << "A()" << std::endl; throw std::exception(); }
    ~A(){ std::cout << "~A()" << std::endl; }
};

int main()
{
    A a;
}

对于 AB, 析构函数并没有被调用。

实际输出:

B()
A()
terminate called after throwing an instance of 'std::exception'
  what():  std::exception
bash: line 7: 21835 Aborted                 (core dumped) ./a.out

http://coliru.stacked-crooked.com/a/9658b14c73253700

如果块作用域变量在初始化期间抛出异常,那么我们是否会得到未定义行为?


4
你抛出了一个异常却没有捕获它,所以程序终止了。你认为这是未定义行为吗? - Beta
3个回答

64
不,抛出异常是在对象构造过程中发生错误的最佳信号方式。(由于没有返回值,因此除了构造一个无头对象(这在C++中是不好的风格)之外,没有其他方法。)从Bjarne Stroustrup本人: http://www.stroustrup.com/bs_faq2.html#ctor-exceptions
(如果您正在工作的项目中不允许使用异常,则必须使构造函数具有不可失败性,并将可能失败的任何逻辑移入具有返回错误可能性的工厂函数中。)
关于“但我的析构函数没有被调用”
确实。 在C++中,对象的生命周期从构造函数运行到完成时开始。当调用析构函数时,它就结束了。如果构造函数抛出异常,则不会调用析构函数。
(但是,已经运行完成其构造函数的任何成员变量对象的析构函数都会被调用。)
请参考标准或一本好的教材以获取更多细节,特别是涉及继承时会发生什么。作为一个经验法则,析构函数的调用顺序与构造顺序相反。
关于您的具体代码中为什么没有调用“~B”,这是因为您在主函数中没有捕获异常。如果您修改代码使得主函数捕获异常,则“~B()”将被调用。但是,当抛出一个没有捕获的异常时,实现可以自由地终止程序而不调用析构函数或销毁静态初始化对象。
C++11标准的参考(重点在下面):
在某些情况下,必须放弃异常处理以使用更少微妙的错误处理技术。
...
在这种情况下,调用std::terminate() (18.8.3)。在找不到匹配的处理程序的情况下,实现定义了在调用std::terminate()之前是否展开堆栈。
作为旁注,一般来说,在您的示例程序中,使用gcc和clang时,将仍然调用~B,而使用MSVC时,~B将不会被调用。异常处理很复杂,标准允许编译器作者在这方面实验并选择他们认为最好的实现,但他们不能选择给出未定义行为。
如果在这种情况下调用析构函数真的很重要,那么您应该确保在main中捕获异常,以便您的代码可移植(在所有符合标准的编译器上都能工作)。例如:
int main() {
    try { 
        A a;
    } catch (...) {}
}

这样,像MSVC这样的编译器将在退出之前调用B的析构函数。


1
你应该查阅标准或好的教材以获取更多细节。我做了6.7/4,唯一能找到的是:如果初始化通过抛出异常退出,则初始化未完成,因此将在下次控制进入声明时再次尝试。 - stella
我现在会尝试找到一个关于实现自由的参考资料,以便在未捕获异常发生时不破坏事物。 - Chris Beck
1
没有问题 :) 我会把参考放进答案里。 - Chris Beck
这个答案很好。可能值得一提的是,如果一个对象在其构造函数中抛出异常,那么成功构造的成员将按照与声明顺序完全相同的相反顺序被销毁。 - Nir Friedman
1
为了让它更清晰一些...这将调用析构函数~B():int main() { try { A a; } catch (...) { throw; } int dummy = 42; } - Tunichtgut
显示剩余5条评论

6

在构造函数中抛出异常是错误处理的标准方式,而不是未定义行为。如果您在构造函数中抛出异常,那么就假定对象没有被正确初始化,因此其析构函数将不会被调用。


1
但是 B b 是存在的。所以,应该是析构函数,对吧? - stella
@stella 这里有一个示例以便理解 - Gelldur

3
这里有一个检查销毁顺序以及发生时间的示例。
#include <iostream>
#include <stdexcept>
using namespace std;

struct KillMe {
    const char* name;
    KillMe(const char*n): name{n} {clog<<"Hello "<<name<<endl;}
    ~KillMe() {clog<<"Bye "<<name<<endl;}
};
struct CantLive : KillMe {
    KillMe fool{"Fool"}, barf;
    CantLive(): KillMe{"Dady"}, barf{"Barf"} {
        clog<<"Dady cannot live..."<<endl;
        throw logic_error("CantLive cannot live");
    }
};

int main() {
    try {CantLive catch_me;}
    catch(...) {clog<<"Gotcha!"<<endl;}
    clog<<"Now let's try without catcher..."<<endl;
    CantLive dont_catch_me;
    return 0;
}

看看构造和破坏发生的方式:

Hello Dady
Hello Fool
Hello Barf
Dady cannot live...
Bye Barf
Bye Fool
Bye Dady
Gotcha!
Now let's try without catcher...
Hello Dady
Hello Fool
Hello Barf
Dady cannot live...
terminate called after throwing an instance of 'std::logic_error'
  what():  CantLive cannot live
exited, aborted

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