The Implications of Leaving in a Destructor
This article explains the risks of adding a destructor to a T class, and (in particular) why stack-based T class objects must never have leaving destructors. It further recommends that none of your classes should ever leave in their destructors, irrespective of type.
Historically (prior to Symbian OS v9.1) the destructor of a stack-based object was not called in the event of a leave - instead the object was simply removed from the stack. Since classes used on the stack could not be leave-safe if they relied on a destructor for cleanup, this lead to the rule that T and R classes must not have destructors.
In Symbian OS v9.1, the underlying implementation of Symbian platform leaves was changed to use C++ exceptions (see A Comparison of Leaves and Exceptions). Now, if a leave occurs, the destructors of stack-based objects are called. So if you work with only Symbian OS v9.1 or later, you can create a T class with a destructor that will be called as the stack unwinds when a leave occurs.
This makes it possible for T classes to own external data or handles since they can be cleaned up by a destructor call, even in the event of a leave. This introduced the option of using a T class as the equivalent of an auto_ptr, something that had previously been limited.
However, if you do add a destructor to a T class, there is a limitation that you should be aware of:
This article examines the reasons for the rule. So, first of all, let's take a step back and look at how Symbian platform manages nested exceptions.
Symbian Platform Says "No!" to Nested Exceptions
When a C++ exception is thrown, memory must be allocated for the exception object. A nested exception occurs when an exception is thrown while another is already being handled. For the second exception, a separate exception object is required, and if a further exception occurs while handling that one, another exception object is required, and so on. If the number of levels of nesting is unbounded, so is the number of allocations of exception objects. This is unacceptable for Symbian platform to support, particularly given that many exceptions occur primarily because code leaves as a result of an out of memory condition.
So, on Symbian platform, nested exceptions are not supported. When the code runs on hardware, only a single exception can be thrown and handled at a time. If another exception occurs while the first is being handled, abort() is called to terminate the thread. By restricting the memory requirement to a single exception object, memory can be pre-allocated to ensure that the exception object can always be created. In fact, when an exception occurs, if there is enough space on the heap the exception object is allocated there. But if not, the object is created using pre-allocated space on the stack.
Note that nested exceptions are supported on the Windows emulator, because exceptions are implemented using Win32 structured exception handling. Therefore, code using nested exceptions that executes correctly on the emulator will not function as intended when it is deployed to hardware.
A nested exception can easily occur in destructor code. This is because destructors are called as part of exception handling. If they throw an exception, it will be nested if it is thrown during cleanup initiated by exception handling. That's why you should never leave or throw and exception in a destructor.
The Anatomy of a Symbian platform Leave
Let's consider what happens when a leave occurs on Symbian platform. Firstly, User::Leave() is called.
- The cleanup stack is un-wound, and each item on it cleaned up (e.g. destructors are called for C classes at this point, Close() is called on any R class object made leave-safe by a call to CleanupClosePushL(), etc).
- An XLeaveException exception object is created and thrown, resulting in stack-unwinding as part of exception handling.
- The catch block (the TRAP) is executed.
During step 1, the cleanup stack takes responsibility for cleanup of, for example, heap-based C class objects, or it calls Close() on R class objects. At this point, no exception has been raised for the leave and, in the cleanup initiated by the cleanup stack, it is acceptable to leave or otherwise throw an exception, because it won't be nested.
During step 2, the normal stack unwinds and the destructors of stack-based objects are called. If a leave were to occur in one of those destructors, it would generate an exception that would be nested within the XLeaveException exception thrown for the original leave. On hardware, this would cause the thread to be terminated.
At step 3, exception handling is complete. Another leave or exception may occur without being nested in that exception thrown in step 2.
Hence if you write a destructor that leaves, it will not terminate the thread if the destructor is called by the cleanup stack (for example, for C class objects). However, if a destructor is called as the normal stack unwinds, you must not allow leaves or exceptions, because they have the potential to cause a nested exception that will terminate the thread when running on hardware.
The limitation also applies if you are using an approach like auto_ptr to ensure cleanup of heap based objects by using stack unwinding. If a stack-based auto_ptr destructor cleans up a heap-based object, the destructor code it calls must not leave or throw an exception either, since that too will be nested.
From Symbian OS v9.1, you can add a destructor to a T class, since it will be called to clean up a stack-based object in the event of a leave. However, you should not allow a leave or exception to occur in the destructor.
Although it is safe for heap-based C classes used on the cleanup stack to have destructors that leave, it is questionable whether it is ever sensible to leave in a destructor (since this suggests that the cleanup code may not completely execute, with the potential to cause memory or resource leaks). The standard advice for all C++ programmers, regardless of the platform they work upon, is never throw an exception in a destructor (see How can I handle a destructor that fails? for further discussion).
We'd like to thank David Caabeiro, Hamish Willee and Mark Shackman and for their contributions to this article.