如果构造函数抛出异常会怎样?

What happens if a constructor throws an exception?

那我们会得到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

所以构造函数在块作用域变量初始化期间抛出的任何时候,我们得到 UB 吗?

不,抛出异常是在对象构造期间发出错误信号的最佳方式。 (因为没有 return 值,除了构造一个无头对象之外别无他法,这在 C++ 中是一种糟糕的风格。)

来自 Bjarne Stroustrup 本人:http://www.stroustrup.com/bs_faq2.html#ctor-exceptions

(如果你在一个不允许异常的项目中工作,那么你必须让构造函数绝对可靠,并将任何可能失败的逻辑移动到有可能 return 的工厂函数中错误。)

回复:“但是我的析构函数没有被调用”

确实如此。 在 C++ 中,当构造函数运行完成时,对象的生命周期就开始了。当析构函数被调用时它就结束了。如果 ctor 抛出,则不调用 dtor。

(但是调用任何成员变量对象的dtor,其ctor在this ctor 运行之前已经运行完成。)

你应该参考标准,或者 good 教科书了解更多细节,尤其是。与涉及继承时发生的情况有关。作为一般经验法则,析构函数的调用顺序与构造函数的相反顺序。

你的问题为什么在你的特定代码中没有调用“~B”,这是因为你没有在main.catch中捕获异常。如果您更改代码以便 main 捕获异常,则将调用“~B()”。但是,当抛出没有捕获的异常时,实现可以自由终止程序而不调用析构函数或销毁静态初始化的对象。

C++11 标准中的参考(强调我的):

15.5.1 The std::terminate() function [except.terminate]

1 In some situations exception handling must be abandoned for less subtle error handling techniques.

...

2 In such cases, std::terminate() is called (18.8.3). In the situation where no matching handler is found, it is implementation-defined whether or not the stack is unwound before std::terminate() is called.

附带说明一下,一般来说,对于 gcc 和 clang,~B 无论如何都会在您的示例程序中被调用,而对于 MSVC,~B 将不会被调用。异常处理很复杂,标准允许编译器编写者可以试验并选择他们认为在这方面最好的实现,但他们不能选择给出未定义的行为。

如果即使在这种情况下调用析构函数对您的程序真的很重要,那么您应该确保在 main 中捕获异常,以便您的代码可移植(在所有符合编译器)。例如:

int main() {
    try { 
        A a;
    } catch (...) {}
}

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

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

这是一个检查销毁顺序的示例。

#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