I encountered some code at work today where an exception is thrown from a destructor.
- You should never throw an exception from a destructor. I point this out because I'm sure if I don't, someone else will.
I am informed that this was a concious design decision and is needed to clean up some data in the case where another failure is encountered. The process of stack-unwinding is being exploited to do the clean up. Under normal circumstances the clean-up process is successful and no exception is thrown. However today I encountered a case where the clean-up failed and an exception is thrown, hence I began to investigate.
The above aside, since this is not a question about code-reviewing code the organization I work for uses, my question is as follows.
What happens if the code path followed as the result of throwing an exception throws another exception?
Since this is an unsual situation I only know of 2 ways this can happen.
The first is the trivial case where an exception is thrown, it is caught by a catch block, and that then throws. This is the same as just throwing an exception which is not caught. There are already some questions about this, for example here. In short,
terminate()
is called.When an exception is thrown, the Stack Unwinding process begins. This process calls destructors of stack allocated objects. Therefore the only other way I know of to cause the throwing of nested exceptions is to throw inside a destructor, in the same manner as I encountered today.
I cannot think of any further possibilities. If there are any I would be interested to hear of them.
Regarding point 2. What happens in this case?
CodePudding user response:
What happens if the code path followed as the result of throwing an exception throws another exception?
That depends on where and when exactly is the second exception triggered. In C , you can have an "unlimited" amount of currently thrown uncaught exceptions and everything must work just fine. This makes the implementation very complicated.
This is not correct,
catch
block can throw an exception withoutterminate
being called, it is even explicitly supported viathrow;
.Depends on what you call "nested", I know 2 more cases leading to
std::terminate
but "nested" exceptions are supported as explained below.- Constructor of the exception object in the thrown expression throws.
- Copy constructor in a
catch
block if captured by value throws.
In general, the exception handling is:
throw
statement is executed, the exception objectE
is created from the expression and stored somewhere, let's say "likely not on the stack".- The matching
catch
block handler for the exception handler is found and the control is transferred to it. If such handler does not exist,std::terminate
is called. - During transferring of the control, relevant local objects are properly destroyed in the reverse order of their construction.
- The
catch
block's argument is constructed fromE
,the exception is now considered caught and the execution ofcatch
block starts. Anythrow
inside the block is a new exception and the process begins anew.
The Standard does not specify the order of 2, 3 . Compilers can first search for the handler and if not found, terminate the program immediately, stack unwinding is not guaranteed to happen in this case. AFAIK gcc,clang do not unwind the stack in this case now.
Stack unwinding calls the destructors of local objects which can call further functions. Those functions can throw leading to more stack unwinding and more destructor calls which again can lead to more exceptions... This is a perfectly valid way how you can have any amount of pending, yet-unhandled exceptions active at the same time. You can actually query that amount by calling std::uncaught_exceptions
The only issue is when the thrown uncaught exception escapes into the stack unwinding process of the previous (still) uncaught, pending exception - i.e. when it is thrown from a destructor directly called during by the stack unwinding algorithm because there wasn't any matching handler found inside. This is the case which leads to std::terminate
call because there isn't much else you can reasonably do.
Throwing from a destructor in itself is supported and can be caught but since you do not know what exactly triggered that call so it is strongly discouraged, actually
if (std::uncaught_exceptions()==0)// Safe throw from dtor.
throw 42;
will work but please pretend you did not read that. One should also ask themselves what it means if an object refused to be destroyed.
CodePudding user response:
The way I phrased my question above makes the answer seem, perhaps, not that surprising.
In particular, when I began researching this, I did not consider the case of an uncaught exception, as in point 1, above.
With this considered, it is perhaps unsurprising that the result is the same in both cases. terminate()
is called.
Here is a link to a Godbolt demonstration. The Executor pane shows that a warning is produced at compile time, and that terminate is called. This information presumably comes back from the operating system.
On further investigation it appears that the terminate()
function by default is setup to call abort()
but that this can be changed.
abort()
then appears to send a signal to the OS, SIGABRT
, which on Linux systems has the value 134 or 6. I found conflicting information on this, I am not sure which is correct.
Here are some references which I will update in time.
https://www.man7.org/linux/man-pages/man7/signal.7.html
https://www.man7.org/linux/man-pages/man3/abort.3.html
I posted this link inline, but it also gives some details about the possible methods which can call terminate()
.
https://www.ibm.com/docs/en/zos/2.1.0?topic=functions-terminate-function
On my Linux test machine, which uses Linux kernel 5.10.0-18-amd64, abort()
prints Aborted
and echo $?
prints 134
.