8

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.

curiousguy
  • 8,038
  • 2
  • 40
  • 58
  • 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 Aug 01 '19 at 08:18
  • Not a C++ expert, but isn't throwing from a destructor "undefined behavior" anyhow? https://stackoverflow.com/questions/130117/throwing-exceptions-out-of-a-destructor (Not having read the question thoroughly, but whatever happens in the scenario that you describe, UB would explain it... because ... that's the problem with UB...) – Marco13 Aug 01 '19 at 12:14
  • 1
    @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 Aug 01 '19 at 12:35
  • 1
    @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 Aug 02 '19 at 03:42
  • 1
    @curiousguy When these people are Bjarne Stroustrup (www.stroustrup.com/C++11FAQ.html#noexcept, "A destructor shouldn't throw") or the ISO committee (https://github.com/isocpp/CppCoreGuidelines/blob/master/CppCoreGuidelines.md#e16-destructors-deallocation-and-swap-must-never-fail , "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 Aug 02 '19 at 11:39
  • 1
    @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 Aug 02 '19 at 17:07
  • 1
    @curiousguy I'm not familiar enough with C++ (and frankly: not bored enough) to argue about that, but 1. they didn't say "We don't know", and 2. it's not about a "cult": These are the people who invented and maintain the language, and if they say something (with reasons), and some random guy on the net says the opposite (without reasons), there's simply no reason for me to believe the latter. If you're not afraid to drive the standard library into UB by throwing from a dtor, that's none of my business ... as long as you're not implementing the emergency shutdown of a nuclear power plant or so – Marco13 Aug 02 '19 at 17:22
  • 1
    @Marco13 They only that dealing with multiple errors at the same time is extremely difficult, which is a trivial observation. Of course it is, nobody claimed it's easy. And I maintained the language too, so... And your claim of getting UB is patently absurd too. Also I perfectly explained the reason: "don't throw" is useless as a recommandation. If you are going to have an operation that can fail, you need to deal with it. As they say in the FAQ **it's hard and there is no general solution**. (Just like multiple dispatch.) – curiousguy Aug 02 '19 at 18:50
  • 1
    Of course the whole point of a dtor is that no `*this` object is left behind, so no postcondition on that object exists. So the user code, say in a template library, really doesn't care about whether destruction "completed". Which leaves a solution: don't do cleanup when you can't. Or use delayed cleanup if cleanup fails, as suggested in the FAQ you cited. Either way, the STL doesn't expect some operation to fail. (`swap` on STL container isn't expected to fail, but I don't see ppl panicking about it like they do with dtors; yet it's the same issue.) So much disinfo on dtors and exceptions... – curiousguy Aug 02 '19 at 19:02
  • 1
    @curiousguy Again, I'm not really in the position to argue here. But I think that e.g. `std::optional` may expose UB when a dtor throws. Beyond that: If you agree that something is "awful" when it is "extremely difficult", the derailing part of this comment discussion seems to be resolved for me. – Marco13 Aug 02 '19 at 21:40
  • 1
    @Marco13 There is no derailing. I'm explaining you what the FAQ means. Failure is a thing. It doesn't matter if we think it's awful, we have to deal with it. That's called being an adult. – curiousguy Aug 03 '19 at 03:20
  • 1
    @curiousguy Being put on spot is sometimes also part of being an adult, and because I'm a curious guy ( :-D ) the simple question for you: May a throwing dtor in a `std::optional` cause UB, yes or no? – Marco13 Aug 03 '19 at 12:48
  • 1
    @Marco13 I don't think there is any std lib component that allows throwing dtors. Also I don't see the relevance of that Q. – curiousguy Aug 03 '19 at 13:30
  • 1
    @curiousguy The relevance is that you said my suspicion that you *might* get UB was "patently absurd", and I wanted to know whether you *know* that, or whether it was just a guess on your side. Some parts of std handle throwing dtors with `std::terminate`, but things become tricky when `std::optional` expects the type to be *Destructible* (i.e. having a non-throwing dtor). Crossing fingers and "hoping that it won't be so bad" at least seems dangerous to me. Wanna continue that in chat? – Marco13 Aug 03 '19 at 15:46

1 Answers1

5

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

francesco
  • 7,189
  • 7
  • 22
  • 49