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.