Stack unwinding and destructors that throw exceptions (in C++)
by yaobin.wen
Frankly speaking, I only have the experience in explicitly thinking about stack unwinding when I program in C++. Although I think stack unwinding is probably a feature of any programming language (e.g., C#, Java, Python) that supports exception handling or error propagation mechanisms, I don’t have the hands-on experience of dealing with stack unwinding in those programming languages so I’ll focus on the C++ examples in this article.
Recap of stack unwinding
When an exception is thrown, the execution control moves from the throw site to the first catch clause that can handle the type of the thrown exception. The exception propagates to the caller hierarchy until the first catch that can handle the exception is found. For example, in the following C++ code:
class E1 {}; // Exception type 1
class E2 {}; // Exception type 2
class A {};
void f1(int n)
{
try
{
A a1;
f2(n);
A a6;
}
catch (E1 e1)
{
// Handle E1
}
}
void f2(int n)
{
try
{
A a2;
if (n < 0)
{
A a3;
// Will be handled by the `catch` in this function.
throw E2();
}
else if (n > 0)
{
A a4;
// Will be handled by the `catch` in the caller function `f1`.
throw E1();
}
A a5;
}
catch (E2 const &e2)
{
// Handle E2
}
}
When the given value of n is less than zero, an exception of type E2 is thrown and it will be caught by the catch (E2 const &e2) handler in f2; if the given value of n is greather than zero, an exception of type E1 is thrown, but because none of the catch handlers in f2 can handle E1, the exception is propagated to f2’s caller f1 and is handled by the catch (E1 e1) handler there.
When an appropriate catch handler is found, the parameter in the catch specification is initialized. In the example code above, the parameters are e1 and e2. catch (E1 e1) receives a copy of the thrown E1 exception, so e1 is initialized by calling E1’s copy constructor; catch (E2 const &e2) receives a (constant) reference to the thrown E2 exception, so e2 is initialized without calling E2’s copy constructor.
The stack unwinding process begins after the catch handler’s parameter is initialized. The stack unwinding process involves the destruction of all the automatic objects that have been fully constructed but not yet destructed between the beginning of the try section that the catch handler is associated with, and line of throw.
In the example code above:
- If
E2is thrown, thecatch (E2 const &e2)handler is called to handle the exception. Because thetrysection that’s associated with thiscatchhandler is inf2, stack unwinding happens insidef2only, and stack unwinding will destructa3anda2because they are automatic objects and have been fully constructed since the beginning of thetrysection and thethrow E2()statement.- Once the exception
e2is handled inf2, the execution control returnsf1. Eventually,a1anda6will also be destructed but they are not destructed by the stack unwinding becausef2has fully handled the exceptione2sof1’s exception handling is not called at all, hence no stack unwinding.
- Once the exception
- If
E1is thrown, thecatch (E1 e1)handler is called to handle the exception. Because thetrysection that’s associated with thiscatchhandler is inf1, stack unwinding happens covers the beginning of thetrysection inf1all the way to thethrow E1()statement inf2. Therefore,a4,a2, anda1are destructed during stack unwinding. In this case, none ofa3,a5, anda6were constructed so they were not destructed.
Exceptions during stack unwinding
If an exception is thrown during stack unwinding, the terminate handler is called and, usually, the C++ program is aborted. According to the previous section, we can see there are two cases in which an exception can be thrown during stack unwinding:
- If the copy constructor of the exception object throws an exception.
- If the destructor of an automatic object throws an exception.
By default, the copy constructor and the destructor of a class is treated to be non-throwing (i.e., noexcept(true)). See “cppreference: Copy constructors” and “cppreference: Destructors”, which both refers to “cppreference: noexcept specifier”. The developers can surely declare them as “potentially-throwing” ones if throwing an exception makes sense in the context. But the fact that they are treated as non-throwing functions by default shows that the C++ language really hopes that the developers can make their best effort to make sure the copy constructor and the destructor do not throw.
The section “Item 8: Prevent exceptions from leaving destructors” in Effective C++ (3rd edition) discusses why this should be done and gives concreate suggestions of how to achieve this goal.
More examples
See Stack-Unwinding/Demo for the demo code.