Home > Blockchain >  Should I use `std::uncaught_exceptions()` to decide whether to throw an exception from my dtor?
Should I use `std::uncaught_exceptions()` to decide whether to throw an exception from my dtor?

Time:11-29

I have a class whose ctor makes a driver call, and whose dtor makes the matching terminating/release driver call. Those calls can fail. The problem is naturally with the dtor.

I am naturally aware of the common wisdom of avoiding exceptions in dtors, since, if you throw one during stack unwinding, you get std::terminate. But - I would rather not just "swallow" such errors and fail to report them - if I can. So, is it legitimate/idiomatic to write code saying:

~MyClass() noexcept(false) {
    auto result = something_which_may_fail_but_wont_throw();
    if (std::uncaught_exceptions() == 0) {
        throw some_exception(result);
    }
}

Or is this just baroque and not a good idea?

Note: This class does not have access to the standard output/error streams, nor a log etc.

CodePudding user response:

If the only thing you do is check whether uncaught_exceptions() is zero, then you can miss some cases where it's safe to propagate the exception. For example, consider

struct X {
    ~X() noexcept(false) {
        if (std::uncaught_exceptions() == 0) throw FooException{};
    }
};

struct Y {
    ~Y() {
        try {
            X x;
        } catch (const FooException&) {
            // handle exception
        }
    }
};

int main() {
    try {
        Y y;
        throw BarException{};
    } catch (const BarException&) {
        // handle 
    }
}

Here, y will be destroyed during stack unwinding. During Y's destructor, there is one uncaught exception in flight. The destructor creates an X object whose destructor subsequently has to decide whether to throw a FooException. It is safe for it to do so, because there will be an opportunity to catch the FooException before it gets to a point where std::terminate will be invoked. But X::~X determines that an uncaught exception is in flight, so it decides not to throw the exception.

There is nothing technically wrong with this, but it's potentially confusing that the behaviour of the try-catch block in Y::~Y depends on the context from which Y::~Y was invoked. Ideally, X::~X should still throw the exception in this scenario.

N4152 explains the correct way to use std::uncaught_exceptions:

A type that wants to know whether its destructor is being run to unwind this object can query uncaught_exceptions in its constructor and store the result, then query uncaught_exceptions again in its destructor; if the result is different, then this destructor is being invoked as part of stack unwinding due to a new exception that was thrown later than the object's construction.

In the above example, X::X() needs to store the value of std::uncaught_exceptions() during its construction, which will be 1. Then, the destructor will see that the value is still 1, which means that it's safe to let an exception escape from the destructor.

This technique should only be used in situations where you really need to throw from destructors and you are fine with the fact that whatever purpose will be served by throwing from the destructor will go unfulfilled if the std::uncaught_exceptions() check fails (forcing the destructor to either swallow the error condition or terminate the program). This rarely is the case.

CodePudding user response:

Under no circumstances should you throw from a c destructor.

From https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/n4659.pdf

18.5.1 The std::terminate() function [except.terminate] ... (1.4) — when the destruction of an object during stack unwinding (18.2) terminates by throwing an exception, or

So its not just 'wisdom' not to throw in a destructor. It will cause your program to crash/exit.

Instead, what I do for this situation, is have a method of the class called something like "Complete".

In the 'Complete' method, you can check for errors codes and throw safely.

You can add a data member (completed) - private - initialized to false, and set true in the Complete () method. And in your destructor, assert its true (so you can catch any cases where you forgot to call Complete).

Throwing from destructors will likely cause grave disorder in your program.

  • Related