Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What causes this weird behavior with throwing destructors during unwinding of a try-block?

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.

like image 294
luizchagasjardim Avatar asked Aug 01 '19 07:08

luizchagasjardim


People also ask

What happens when destructor throws exception?

Throwing an exception out of a destructor is dangerous. If another exception is already propagating the application will terminate.

Can we throw an exception from the destructor?

The C++ rule is that you must never throw an exception from a destructor that is being called during the "stack unwinding" process of another exception. For example, if someone says throw Foo(), the stack will be unwound so all the stack frames between the throw Foo() and the } catch (Foo e) { will get popped.

How can I handle a destructor that fails?

If a failure occurs during destruction of an object and the object cannot handle the failure itself, then throwing is the only option. It is the responsibility of the programmer to catch the exception at a point at which the failure can be handled properly.

What is stack unwinding in C++?

When an exception is thrown and control passes from a try block to a handler, the C++ run time calls destructors for all automatic objects constructed since the beginning of the try block. This process is called stack unwinding. The automatic objects are destroyed in reverse order of their construction.


1 Answers

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

like image 65
francesco Avatar answered Oct 20 '22 01:10

francesco