|Ned Batchelder : Blog | Code | Text | Site|
Destructors instead of catches
» Home : Text
Created 15 February 2004
One of the most powerful features of C++ is the guarantee that stack objects will be destroyed as functions are exited, regardless of why they were exited (normal return, or an exception being thrown). This is one of those clever techniques that is easy to know about, but then easy to overlook anyway.
C++ guarantees that an object on the stack will be destroyed (its destructor will be called) no matter how the function exits. There are many aggravations when coding in C++, and there are many things that more modern languages (Python or Java) will take care of for you. But this powerful feature of C++ is missing in those languages. If you must work in C++, you should at least take advantage of this useful idiom of the language.
The classic use of this idiom is resource allocation. At some point in a function, you allocate a resource. You must ensure that when the function is exited, the resource is deallocated, or the resource will leak. There are many types of resources this scenario could apply to. Examples are heap memory, a lock on a mutex, or a database connection.
For this discussion, let's imagine we have this imaginary resource class:
The manual approach to allocating and deallocating a Resource would be:
The try-catch block does nothing for us here except allow us to clean up the resource if an exception is thrown. Rather than have to clutter the code with try-catch blocks, we can create a stack object that handles this all for us:
(A real implementation would be a little more complex, testing if the pointer is NULL in the destructor, maybe providing explicit methods to let go of the resource and so on. As with the rest of the examples here, this is simplified to make the fundamental intent clearer.)
Now our function can let AutoResource worry about the clean up for us:
There's no mention of exceptions at all now, and the resource is still properly managed when exceptions are thrown.
The simple clean-up-at-the-end object is the most basic use of the auto-destructor technique. But more complex problems can be solved this way as well. Suppose you have some record-keeping to do around an activity. For example, you have functions for tracking the success or failure of processing:
You have a function that does a large chunk of processing, and the functions it calls can throw exceptions. To properly call your three activity tracking functions, you could explicitly catch the exceptions, call your trackers, then continue on:
This works, but is cumbersome, distracting and error-prone. The try-catch block is there only to perform bookkeeping. This function doesn't want to try to handle exceptions at all. Rather than catch exceptions just to perform bookkeeping like this, much better is to use a stack-allocated object whose destructor performs the bookkeeping.
We'll build a special-purpose object whose job is to be destroyed on exiting the function. It maintains state to determine the proper action when it is destroyed:
This object encapsulates the state of the activity, and calls the proper function in its destructor. You simply have to construct one at the start of the function, and let it know when everything has finished properly. Now we can re-write DoSomething so that the exceptions are invisible:
If the code throws no exceptions, then the destructor will call ItWentWell, because Tracker.WentWell() sets the m_bWell member to true. If the code throws an exception, then the object will be destroyed as the exception passes through the routine. The bool member will still be false (as it was set in the constructor), since ItWentWell wasn't called, and the destructor will call ItWentBadly.
C++ has its strengths and its weaknesses. One of the strengths is the guarantee that objects will be properly destroyed. By using this technique to its fullest, you can keep repetitive bookkeeping neatly encapsulated in helper objects which help to keep your main code uncluttered and clean.