8
votes

When the try-block encounters an exception, the stack is unwound. If an object was created inside the try-block, the destructor is called. If the destructor throws another exception, this exception is not caught and the program is terminated.

So if you have:

struct A {
    ~A () noexcept(false) {
        std::cout << "A::~A" << std::endl;
        throw std::runtime_error("A::~A ERROR");
    }
};

And then your try-catch block is something like:

try {
    A a1;
    A a2;
} catch (...) {}

Then when the try-block finishes, the destructor for a2 throws, the exception is caught, then the destructor for a1 throws and the program is terminated. Everything works as expected.

But if you introduce another struct that also throws in the destructor, but inherits from A or has an instance of A as a member, things become confusing. For example, if you have:

struct B : A {
    ~B () noexcept(false) {
        std::cout << "B::~B" << std::endl;
        throw std::runtime_error("B::~B ERROR");
    }
};

Then if you do:

try {
    B b;
    A a;
} catch (...) {}

The expected outcome should be that A::~A is called an the exception is caught, then B::~B is called an the program terminates. But instead, in all compilers I tried except MSVC, the output is:

A::~A

B::~B

A::~A

terminate called after throwing an instance of std::runtime_error

  what():  A::~A ERROR

As if two exceptions were caught and the third terminated the program.

The same also happens if you define B as:

struct B {
    ~B () noexcept(false) {
        std::cout << "B::~B" << std::endl;
        throw std::runtime_error("B::~B ERROR");
    }
    A a;
};

I also tried some other combinations with more structs.

Don't bother putting anything in the catch-block, because the program will never even go there.

And yes, I know that ideally destructors shouldn't even throw exceptions. It's more of a curiosity after having read an article about throwing destructors.

1
Here the output is different. With Mingw-w64 and g++ 5.2 only the destructor of A is called and with g++ 8.1 both, the destructor of A and then the destructor of B get called.Benjamin Bihler
@Marco13 -- throwing an exception from a destructor does not result in undefined behavior, even if that destructor was invoked during stack unwinding. In the latter case, the result is a call to std::terminate().Pete Becker
@Marco13 Throwing in dtors is just considered absolutely awful by many ppl who claim to have a logical argument for why it's awful but really don't.curiousguy
@curiousguy When these people are Bjarne Stroustrup (www.stroustrup.com/C++11FAQ.html#noexcept, "A destructor shouldn't throw") or the ISO committee (github.com/isocpp/CppCoreGuidelines/blob/master/… , "Destructors, deallocation, and swap must never fail"), and they mention reasons like "The standard library assumes that destructors ... do not throw. If they do, basic standard-library invariants are broken.", I'm strongly inclined to believe them.Marco13
@Marco13 To believe them as in a cult? Ideally no function should throw or fail. In the real world they do. "Dtors shouldn't throw" is a mantra and it's useless. What if they fail? "We don't know" (from your FAQ) isn't a useful answer.curiousguy

1 Answers

5
votes

I think the behavior you are observing is implementation-dependent. From the c++ reference on std::terminate() (emphasis mine):

std::terminate() is called by the C++ runtime when exception handling fails for any of the following reasons:

1) an exception is thrown and not caught (it is implementation-defined whether any stack unwinding is done in this case)

In your first case, on exiting the scope:

  • The destructor of A is invoked.
  • The exception std::runtime_error("A::~A ERROR") is thrown.
  • Such an exception is caught by catch(...).
  • On unwinding the stack also the destructor of B is called.
  • At this point the std::terminate() is called. But it is implementation-defined whether

    a) the program immediately terminates and gives the output you expect

    or

    b) the program unwinds the stack, hence calls the destructor of the base class A and then terminates; this is what you see observe.

See the code live on Coliru