by °Jack W. Reeves
In April 1995 the
When I started to use exceptions, it rapidly became apparent that exceptions are much more difficult to use effectively than it first appears. In fact, the more I explored how exceptions can interact with non-exception handling code, the more convinced I became that exceptions may be the most difficult feature of C++ to use. I identified 3 reasons for this
It is the first of these that is the real problem. When using other features of C++ such as classes, overloaded operators, virtual functions, or templates, it may be difficult to write the code correctly in the first place, but once written it is easy to use from then on. Exceptions go against this trend. Throwing an exception is easy; writing the code that uses the function that throws the exception is the hard part. Every function that can have an exception propagate out of it must be designed to propagate that exception correctly. As Tom Cargill says in Reference 1, "The really hard part of using exceptions is to write all the intervening code in such a way that an arbitrary exception can propagate from its throw site to its handler, arriving safely and without damaging other parts of the program along the
Of course, it is (ii) and (iii) that make writing all that intervening code difficult. Consider the apparent asynchronous nature of exceptions (exceptions are not truly asynchronous events like a network driver interrupt, but from the outside, they can have almost the same effect). The following
while (*i++ = *j++) ;
is a typical C idiom that has been carried over (with some major extensions) into C++. In fact, one can almost say that the i
and j
can be two different, user-defined iterator types. Assume that i
iterates over a container of A objects, and j
iterates over a container of B objects. Let's let a represent *i
and b represent *j
. Assignment will have been overridden if A is a user defined type, but it may not be defined specifically for the type represented by B, necessitating invocation of a user-defined conversion function. Another user-defined conversion function may have to be invoked to convert the result of the assignment statement into something that can be converted to type bool
for the "while" condition. Any of these user defined operators or conversions might throw an exception or any of the functions that they invoke, and so on. If an exception occurs in this statement, the user may have no idea where it came from. More importantly, the order of evaluation of several parts of the above expression is unspecified. Therefore, if an exception appears, the user may have a hard time determining the current state of i
, j
, a
, or b
.
Finally, as noted in (iii), exceptions can not be ignored. With older error reporting techniques, nothing but discipline forced the programmer to check the return value or error code. Even if you knew errors were possible, you did not have to check the return value immediately, just as long as it was checked before it mattered. With exceptions, this is no longer possible. You have to cope with an exception when and where it occurs, not when and where it is convenient for you. This is true even if you do not plan to actually handle the
So, I have concluded that using exceptions effectively is going to be difficult. Nevertheless, I have also considered the alternatives. Without exceptions it is impossible to return an error from a constructor or an overloaded operator. This situation has lead to several ad-hoc schemes. A couple of the more popular ones
operator!
function or a conversion to bool
or void*
is often provided to simplify testing the state of the object. Such a scheme can be used to tell if a constructor (or an operator) succeeded or failed, but it does not tell anything about why the failure occurred. For that, a member function that returns the value of an error code is usually provided.
Since there is no standardization for any of this, it is difficult to combine libraries that use slightly different techniques. It also is virtually impossible to use such classes to instantiate general purpose templates like those that are now part of the
What follows are some goals and guidelines that I have put together on how to cope with exceptions. The "goals" are general, high level concepts. Meeting a "goal" may involve re-writing a function or major changes to a class library. The "guidelines" are more specific, lower level concepts that can often be applied to help meet the goals. As with most such offerings, every user will have to evaluate their own circumstances and determine which of these goals and guidelines are applicable to their situation. In many cases, I have found that exceptions are already approaching the status of religion. Under the circumstances, I try to offer reasonable explanations for my recommendations. Nevertheless, my
ultimate goal is more reliable software, not religious discussions. If
other approaches work for you, by all means use them. I would like to hear
from readers who have applied these and other guidelines on real projects
successfully or not. The more collective knowledge we can build on this topic, the better off we will all
In the following, I often mention the "state of an object". In Reference 2, Dr. Harald Müller presents a more general discussion of the problems I discuss in goals I through III. In his article, Dr. Müller talks about "resources". I have decided to stick with the term "object" since that is the type of resource most C++ programmers are going to be concerned with most often (I do use the term "resource leak"). In Dr. Müller's article, he defines and discusses how resources can be in either "good" or "bad" states. While my concepts are similar, I prefer slightly different terminology than Dr. Müller. In simple terms, an object can be in one of the following
My definitions correspond fairly closely with Dr. Müller's as
Dr. Müller | Mine |
---|---|
good | good |
bad (valid shut-down) | bad |
bad (not a shut-down) | undefined |
I differ primarily with Dr. Müller over the question of what operations are valid when applied to an object that is not in a good state. In the real world of typical C++ software, an object in an inconsistent or bad state may still be quite usable and the program in which the object is embedded may continue to run
For certain examples in the rest of this article, I use the same Stack
template class as Dr. Müller uses in his paper. The original version of this template was presented by Tom Cargill in Reference 1. The interface for this class is shown in Listing 1. While the Stack
class is fairly simple, it remains a good example of the difficulties inherent in using
A couple of brief notes about coding
null
pointer in my code instead of the current convention of using 0
. I ask the reader to assume the following declaration exists within the scope of all the code that follows:
const long null = 0L; // see ONMOUSEOVER = "self.status = 'Item E25'; return true" ONMOUSEOUT = "self.status = self.defaultStatus">Item E25 for a discussion // of this approach
//>x
to indicate places in a function where exceptions are possible. Like any manual approach, this technique is error prone and very likely to miss something. It is better than nothing, however, and I have found it useful.
The following table summarizes the goals and guidelines that are presented in the next
Catching and Propagating Exceptions
We must state some assumptions before we actually start to deal with exceptions. We will assume that an exception thrown by a lower level routine indicates that the routine failed. In a follow-on article [Reference 3], I present some goals and guidelines for throwing exceptions. While there is no way to ensure that exceptions are only used to indicate a failure condition, (and there are a couple of situations where they may not), it is probably safe to assume that most programmers are not going to accept the overhead inherent in an exception throw (see Items ONMOUSEOVER = "self.status = 'M12'; return true" ONMOUSEOUT = "self.status = self.defaultStatus">M12 and ONMOUSEOVER = "self.status = 'M15'; return true" ONMOUSEOUT = "self.status = self.defaultStatus">M15) if other mechanisms are available to indicate normal
We will also assume that exceptions are typically a rare occurrence in a well designed program. This means, in general, that the condition causing the error is not likely to be one that is easily anticipated and handled by the next higher level routine. The entire exception handling mechanism is designed to pass information from the point where an error is detected up the calling chain to a higher level of the program where enough information exists to be able to handle the error. In many cases, exceptions are going to propagate all the way to a human operator. Therefore, it is probably safe to say that the vast majority of code will simply propagate exceptions from lower level routines to higher level
It is this propagation of exceptions that causes the grief and anguish. The function that throws the exception knows what state it is in and it chooses when to throw the exception. The function that finally handles the exception remains in control it must necessarily branch into a separate exception handling path but in handling the exception it recovers from the error and continues. All of the intervening functions between the one that originally threw the exception and the one that finally handles it are a problem. When an exception propagates out of a function, it indicates that the function failed. It is as if the function threw the exception, but without any control over where the throw occurs. The propagating exception terminates the function, unwinds its stack, and continues on its
If not anticipated and handled correctly, this propagation of an exception can leave dangling resources, bad or even undefined states, and in general cause more problems than the original error. Propagating exceptions destroy otherwise perfectly rational flow of control through a program. In a very real sense, exceptions are "goto's from hell". So how do we cope with
Goal I. When you propagate an exception, try to leave the object in the state it had when the function was entered. This is the Golden Rule of exception handling. It is not going to do much good for a program to handle an exception if the program has gone into an invalid state as a result of the exception propagating to the point where it could be handled. Writing code that meets this goal is very difficult. In truth, it may often be impossible. Nevertheless, I put it first because it is the target that should always be the ultimate goal. How to go about this is a complicated subject, and there are more detailed articles on dealing with it [Reference 2]. I will only give a few simple guidelines
Guideline
1. Make sure your const functions really are const. The first step in making
sure objects stay in good states is determining which operations on the object
actually change the state. The C++ language assists us in this regard by requiring
member functions that can be applied to constant objects to be designated as
const functions (see
ONMOUSEOVER = "self.status = 'Item E21'; return true" ONMOUSEOUT = "self.status
= self.defaultStatus">Item E21). As we all know however, const can be subverted,
either by designating some members mutable (again, see
ONMOUSEOVER = "self.status = 'Item E21'; return true" ONMOUSEOUT = "self.status
= self.defaultStatus">Item E21), or with a const_cast
(see
ONMOUSEOVER = "self.status = 'Item M2'; return true" ONMOUSEOUT = "self.status
= self.defaultStatus">Item M2). If your const
functions
really do not change the state of the object, then you can safely ignore them
exceptions that propagate from them will not affect
On the other hand, if your const
functions do change the state of the object, then you must treat your const functions like your non-const
functions only more so. As a user, I would be very annoyed if I discovered that a const
function could actually change the object into a bad state under some circumstances. If you discover such a function in your class, I would urge reconsidering whether it should be a const
function. An example of this actually occurs in the ONMOUSEOVER = "self.status = 'Standard C++ Library'; return true" ONMOUSEOUT = "self.status = self.defaultStatus">Standard C++ Library. The basic_string
function c_str()
is a const function, but the specification is clear that an implementation can reallocate the internal character array if necessary to make room for the terminating NULL
character. This could throw a bad_alloc
exception. When a const
function exhibits such behavior, it has numerous ramifications, most of them
Guideline 2. Perform exception prone operations early, and/or through temporaries. Consider the Stack::push
template<class T> Stack<T>::push(T element) { if (top_ == nelems_) { T* new_buffer = new T[nelems_*= 2]; //>x1 for (int i = 0; i < top_; i++) new_buffer[i] = v_[i]; //>x2 delete[ ]v_; //>x3 v_ = new_buffer; } v_[top_++] = element; //>x4 }
Each of the comments indicates a statement that potentially can throw an exception. For example, the statement at x1
can throw a bad_alloc
exception if there is not enough memory to satisfy the request. In this statement, the member variable nelems_
is increased, and then used as the size of the new request (a typical C idiom). If this request fails and throws an exception, it propagates out of push()
leaving nelems_
set to the new value. The Stack
object is now in an bad
Besides doing memory allocation, statement x1
also uses T::T()
. Similarly, statements x2
and x4
use T::operator=()
, and x3
uses T::~T()
. Each of these functions might throw an exception. For now I will ignore the possibility of exceptions from destructors, but x1
, x2
, and x4
are all potential problems. If we propagate an exception from one of these operations it will indicate that the push()
operation failed, but in what state will we leave the Stack
An exception caused by a failure in T::T()
at x1
has the same effect as a bad_alloc
exception. If we use a temporary variable for the new value of nelems_
, then an exception from x1
can safely be allowed to propagate. We will assume that if T::T()
throws an exception the runtime mechanism will destroy all the already constructed elements in the array and release the allocated memory. (Except in pathological cases, which are discussed below, the standard guarantees this
If one of the assignments in the loop at x2
fails, the internal state of the Stack
object is still good, but we have a memory leak since the buffer pointed to by new_buffer
will never be deleted. We will discuss resource leaks in more detail under Goal IV
If the assignment at x4
fails, variable top_
will already have been incremented. In this case, the exception will indicate that the push()
failed, but the internal state of the object will have been updated as though it succeeded. Again, we are in a bad
In order to make sure that possible exceptions that propagate out of push()
leave the object in its original state (and do not cause a memory leak), a number of modifications are
auto_array_ptr
(see below) to create an automatic variable that manages the new buffer.
auto_array_ptr object
and the stack.
top_
variable until after the new element is assigned to the stack buffer.
Class auto_array_ptr
is similar to the auto_ptr
template class defined in the utilities section of the C++ Standard Library (See Items > 9 and > 28). An auto_ptr
is an object that owns a pointer. The destructor for auto_ptr
deletes the object pointed to. My definition of auto_ptr
is shown in Listing 2. The header for auto_array_ptr
is shown in Listing 3 (its definition is almost exactly the same as for auto_ptr
except that it calls delete[]
on its pointer instead of delete
). Using an auto_array_ptr
object will solve our memory leak by automatically deleting the array if an exception occurs. When the new array has been initialized with copies of the elements from the old buffer (an exception prone task), the ownership of the new buffer is assigned to the stack object. Lastly, we re-write the assignment at x4
so that we do not update the state of the object member top_
until after the potentially dangerous assignment has completed. All this gives
template<class T> Stack<T>::push(const T& element) { if (top_ == nelems_) { size_t new_nelems = nelems_ * 2; auto_array_ptr<T> new_buffer = new T[new_nelems]; //>x1 for (int i = 0; i < top_; i++) new_buffer[i] = v_[i]; //>x2 v_ = new_buffer.reset(v_); //>x3 nelems_ = new_nelems; } v_[top_] = element; //>x4 top_++; }
The statement at line 3 swaps ownership of the old buffer and the new buffer. After the statement, the auto_array_ptr
object owns the old buffer and the Stack
object owns the new buffer (via its member v_
). The new_buffer
object will then delete the old buffer when it is destroyed at the end of the block. I do it this way so that if an exception occurs when the old buffer is deleted, the stack
object will still be in a good
Even this simple example illustrates a key difficulty in coping with exceptions. When writing a function, it is necessary to decide which operations might cause exceptions and which operations are exception-safe. Exception specifications (which are discussed in a later article [Reference 4]) can help in this task, but they are not a complete answer (see ONMOUSEOVER = "self.status = 'Item M14'; return true" ONMOUSEOUT = "self.status = self.defaultStatus">Item M14). In particular, template programmers do not have access to the exception specifications of classes used to instantiate the template. (For a further discussion of this topic, consult Herb Sutter's article, "Exception-Safe Generic Containers"). Once you get into the mind set of expecting exceptions, the problem often becomes trying to determine which statements are NOT possible sources of exceptions. The only operations that can safely be assumed to never throw exceptions are the basic operations on the built-in data types. In the example above, the call to auto_array_ptr::reset()
is safe because all it does is swap two built-in pointer types through a
The auto_array_ptr
object automatically deals with the possible exceptions in the loop at line x2
. An exception prior to line 3 will now propagate out of the function without causing either a bad state or a memory leak. That leaves only the possible exception from the assignment at line x4
to be dealt with, which brings up Guideline 3.
Guideline 3. Avoid side effects in expressions that might propagate exceptions. Side effects are a fact of life in C++. Some of them we can not see directly (and can not do anything about). Others we can see. In statement x4
, whatever happens inside T::operator=()
are of the former type, top_++
is one of the latter. As it was originally written, an exception from T::operator=()
was guaranteed to leave us in a bad state (at least the state might have been even worse, i.e. undefined) because top_
would have been incremented before T::operator=()
was called. Since T::operator=()
might throw an exception, we must avoid the side effect to top_
by postponing the
Note that even with this change, we can not be sure what state the Stack
object is in if an exception occurs at line x4
it depends upon what state T::operator=()
leaves the object at v_[top_]
. If an exception thrown by T::operator=()
leaves the T
object in a good state, then we can say that the Stack
object itself is in a good state. If the assignment leaves the T
object in a bad state, then we must consider the Stack
object to also be in a bad state we can not expect another call to push()
to work if the stack buffer now contains a bad object. (In this case, even though the stack itself is in a bad state, we can still pop()
those objects that currently exist on the stack. In fact, as long as we do not try to push()
an object into the slot that now contains the bad T
subobject, we can still use our stack as if it were good, even though it isn't. This is often the case in real software
In the final case, if an exception leaves the T
object in an undefined state, then we must consider the Stack
object to be in an undefined state. This means that it probably will not be possible to destroy the stack. While there is no way to tell what state the object at v_[top_]
is in if an exception occurs at line 4, we want to make sure that if class T
adheres to Goal I, then the stack object does also. For a slightly more complicated example consider the Stack::pop()
function. In its original
template<class T> T Stack<T>::pop() { if (top_ == 0) throw "pop on empty stack"; return v_[--top_]; //>x1 }
The return statement uses the copy constructor of the template class T
. If T::T(const T&)
throws an exception, it will again do so after top_
has been changed. The exception will indicate that pop()
failed, but the internal state will have been changed as though it succeeded. While it might be argued that the stack is still in a good state in this case, it does violate our goal we want to leave top_
as it was when we entered the function. Since we can not decrement top_
after the return statement, we must catch the exception and reset the state (we will also throw a more appropriate exception for the Stack
empty
if (top_ == 0) throw domain_error("empty stack"); try { return v_[top_]; } catch (...) { top_++; throw; }
The domain_error
class is one of the Standard library exception classes (see ONMOUSEOVER = "self.status = 'Item M12'; return true" ONMOUSEOUT = "self.status = self.defaultStatus">Item M12). It is derived from logic_error
, which is in turn derived from exception
. As discussed in the second article of this series [Reference 3], library exceptions should derive from the standard exception classes. Class domain_error
is a general purpose base class that indicates that the domain of the attempted operation is invalid. For this example, I chose to throw a domain_error
object directly rather than derive a new, Stack
specific, domain_error
Goal II. If you can not leave the object in a same state it had when the function was entered, try to leave it in a good state. In other words, even if we lose the old state of the object, we would like to be able to reuse the object. This is often perfectly acceptable with the assignment operator since we expect to lose the old value of the object when we assign a new value to it anyway. Consider the Stack
assignment operator (in its initial
template<class T> Stack<T>& operator=(const Stack<T>& s) { if (&s == this) return *this; delete)v_; //>x1 v_ = new T[nelems_ = s.nelems_]; //>x2 for (top_ = 0; top_ < s.top_; top_++) v_[top_] = s.v_[top_]; //>x3
We have several possible exception sites in this function. As before, I will ignore the possibility of exceptions from the destructors invoked in x1
. Line x2
uses the default constructor for class T
, and line x3
uses T
's assignment operator. We can (and will) re-write this function to meet Goal I, but for now observe that if an exception propagates from line x2
we are in an undefined state (worse than 'bad'). The old array has been deleted, but an exception at x2
means we failed to allocate its replacement. Pointer v_
is left dangling and if we try to destroy the Stack
object,delete will be invoked on this dangling pointer. If we do nothing else, we want to make sure that we can always destroy the object (Goal III which we will get to later). This shows just how easy it is for a propagating exception to leave an object so messed up that it can not even be safely
If we get past line x2
and hit an exception in line x3
we are in a more interesting position. The Stack
object is now actually in a consistent state, but since we only have a partial copy done, the object is not valid. The exception propagating from the function will indicate that the assignment failed, but at this point, since we have deleted the old stack
data, we can not restore the object to the state it had before the function was called. The only valid things that can be done with this stack
object are to destroy it, or reassign a valid stack
to it. I emphasize the word valid. This is a case where the internal state of the object appears good, and hence the typical operations could be invoked and would appear to work, but the meta-state (for lack of a better term) is inconsistent. In this case, the stack
object does not represent a valid last-in-first-out ordering of the objects on the stack because it was truncated in the middle of the copy of a valid
As noted, we can fix the assignment so that it meets Goal I, but let us assume that we could not do
Guideline 4. Either reinitialize the object, or mark it internally to indicate that it is no longer usable but might be recovered. If the Stack
object can not be restored to its original state, we want to make sure that any further attempt to use the object will be rejected. The user might handle the exception, but not realize that the object is no longer valid. In the case of our Stack
assignment operator, we can simply reinitialize the object to the empty state. In more complicated cases, we may want to leave the object in the state it was in until the user explicitly clears the condition. This makes sure the user is aware of the problem. Our Stack::operator=()
now
template<class T> Stack<T>& operator=(const Stack<T>& s) { if (&s == this) return *this; auto_array_ptr::remove(v_); //>x1 top_ = 0; nelems_ = 0; // reinitialize object v_ = new T[s.nelems_]; //>x2 for (size_t i = 0; i < s.top_; i++) v_[i] = s.v_[i]; //>x3 nelems_ = s.nelems_; top_ = s.top_; return *this; }
Line x1 deletes the array through a temporary (auto_array_ptr::remove()
is discussed below) to guarantee that the object can be destroyed in case of an exception. Line x2 is now safe, and any exception will leave the object in a valid empty state. Likewise, an exception at line x3. As noted above, we can rewrite this function to meet Goal I, but this is a good
Goal III. If you can not leave the object in a "good" state, make sure the destructor will still work. While the ultimate goal is to leave an object in a good state (Goal I or II) it may not always be possible. As a last resort, we want to leave the object in such a state that it can be safely destroyed. Never forget that as an exception propagates it will unwind the stack frame of every function it propagates through. Many times this will invoke the destructor of the very object that threw the exception. One question that should always be asked when attempting to cope with exceptions is: "What will happen if this exception attempts to destroy this
As our original Stack::operator=()
function showed, it is not difficult to leave the object in a state where even the destructor will not work. The most common cause of this is a dangling
Guideline 5. Do not leave dangling pointers in your objects. Delete pointers through temporaries. In the Stack
assignment function above, the simplest solution to the dangling pointer was to set v_
to null after the delete
statement. Since I figure this is going to be such a common occurrence, I added a static function to my auto_ptr
(and auto_array_ptr
) class to facilitate this operation. Calling auto_ptr::remove(p)
(auto_array_ptr::remove(p)
) will delete pointer p
through a temporary and leave p
set to null. Even if an exception occurs during the destructor call, p
will not be left dangling. If you use auto_ptr
objects as members of your class instead of raw data pointers (see ONMOUSEOVER = "self.status = 'Item M10'; return true" ONMOUSEOUT = "self.status = self.defaultStatus">Item M10), the task becomes even easier. If ap
is an auto_ptr
object,
delete ap.release();
will set ap
to null internally before returning the pointer so it can be
Goal IV. Avoid resource leaks. The most obvious type of resource leak is a memory leak, but memory is not the only resource that can leak (I once had a program that tended to leak TCP/IP sockets). In one sense, a resource leak is just another example of a bad state. In this case though, the resource that is in the "bad" state is the one that has leaked, not the one that did the leaking. For this reason, I deal with it
There are three different instances where exceptions can cause resource leaks: in a constructor, in a destructor, and in a function (whether a member of a class or not). Let us look at constructors
Constructors are a special case for the exception handling mechanism. When an exception propagates from a constructor, the partial object that has been constructed so far is destroyed. If necessary, any memory allocated from the free store for the object is also released. Note that the destructor for the object itself is not called only destructors for any completely constructed subobjects. If, during the construction, a resource (such as memory) is obtained directly and not as part of a subobject whose destructor will release the resource, then a resource leak can occur (again, see ONMOUSEOVER = "self.status = 'Item M10'; return true" ONMOUSEOUT = "self.status = self.defaultStatus">Item M10). For
class T1 { ... }; class T2 { ... }; class X { T1* p1_; T2* p2_; public: X(); ~X(); }; X::X() { p1_ = new T1; p2_ = new T2; // exception causes leak }
If an exception is thrown by T2()
during the initialization of p2_
, then the T2
object will be destroyed, and the memory obtained by new will be released, but not the pointer held in p1_
. We have a memory
There are a couple of ways this can be dealt with. We could use a try block to catch the exception and attempt to release the memory, but if we have several resources allocated this way, then the nested try blocks can get tedious and error prone. An alternative is to make sure the pointers are initialized to null, and then delete them all in the catch
X::X() try : p1_(null), p2_(null) { p1_ = new T1; p2_ = new T2; } catch (...) { delete p2_; delete p1_; // reverse order throw; // redundant }
This example uses several features from the new Standard, so do not expect this to work on your compiler yet. Placing the try
keyword immediately after the parameter list (or the exception specification if one exists), and the catch clauses after the function body, produces a function try block. A function try block associates a handler with the entire function body, including the constructor initializer list. In this example, the initializer list is not the concern, but you can see that the initializer list is within the scope of the try
. With any pointer not set in the constructor body guaranteed to be null, the catch block can safely invoke delete on them. (The throw
in the catch block is redundant. A catch clause of a function try block for a constructor or a destructor will automatically rethrow the exception when it finishes. As a matter of style, however, I always explicitly rethrow exceptions caught by a catch
(...)
clause (Guideline 10).) All this leads
Guideline 6: If you have raw data pointers as members, initialize them to null in the initializer list of your constructor(s), then do necessary allocations in the constructor body where a catch
block can deal with potential resource leaks. This is one possible way to deal with a potential resource leak in a constructor. Another technique is to use the "resource acquisition is initialization" strategy. In this case, we make sure that every resource is associated with an object whose destructor will deallocate it. For dealing with memory allocated from the free store, the standard library template class auto_ptr
(Listing 2) is available. Applied to our
class X { auto_ptr<T1> ap1_; auto_ptr<T2> ap2_; public: X(); ~X() {}; }; X::X() : ap1_(new T1), ap2_(new T2) {}
X::X() { ap1_.reset(new T1); ap2_.reset(new T2); }
Now, since ap1_
and ap2_
are both objects, if an exception occurs trying to initialize ap2_
, then the stack unwind will destroy ap1_
, which will call delete on the allocated pointer. In this case, our destructor is empty since the destructors of member objects are invoked
Besides having a destructor that will delete the resource, template auto_ptr
also provides functionality for safely transferring ownership of a resource. We have made use of this capability several times already. In this case, we can use an "acquire then transfer ownership" strategy to give us the following version of X
's
class X { T1* p1_; T2* p2_; public: X(); ~X() {delete p1_; delete p2_;} }; X::X() : p1_(), p2_() { auto_ptr<T1> t1(new T1); auto_ptr<T2> t2(new T2); p1_ = t1.release(); p2_ = t2.release(); }
The auto_ptr
objects are used to acquire the resources in a manner that guarantees they will be deleted if an exception occurs. When all resources have been successfully acquired, ownership is transferred to the class itself. This is just Guideline 2 applied to resource acquisition. I see this as a transitional strategy for constructors, however. In the long run, I suspect that the use of raw data pointers as class members will diminish in favor of auto_ptr
style objects. This simplifies maintenance as well as the problems of coping with exceptions. If you worry about performance, keep in mind that all the operations of auto_ptr
are inline functions (see ONMOUSEOVER = "self.status = 'Item E33'; return true" ONMOUSEOUT = "self.status = self.defaultStatus">Item E33). Most of these are one line functions that any decent compiler should have no trouble
Resource allocations in ordinary functions can also cause resource leaks. Whereas a constructor is building an object and the goal is to make sure everything already acquired is released if an exception occurs, a function usually obtains a resource for internal use and releases it upon completion. If an exception happens after the resource is acquired but before it is released, then we have a leak. Under the discussion for Guideline 2, we used an auto_array_ptr
object in Stack::push()
to manage the new buffer while it was being initialized. We can do the same thing in Stack::operator=()
:
template<class T> Stack<T>& Stack::operator=(const Stack<T>& rhs) { if (this == &rhs) return *this; auto_array_ptr<T> new_buffer = new T[rhs.nelems_]; //>x1 for (int i = 0; i < rhs.top_; i++) new_buffer[i] = rhs.v_[i]; //>x2 v_ = new_buffer.reset(v_); // swap ownership nelems_ = rhs.nelems_; top_ = rhs.top_; return *this; }
If v_
were an auto_array_ptr
object instead of a raw pointer we would use the Standard template function swap()
to perform the exchange of
swap(v_, new_buffer);
Finally, we can have resource leaks in destructors. As a general rule, we do not want to throw (or propagate) exceptions from destructors (see ONMOUSEOVER = "self.status = 'Item M11'; return true" ONMOUSEOUT = "self.status = self.defaultStatus">Item M11 and Herb Sutter's article on "Exception-Safe Generic Containers"). Nevertheless, we can not always prevent it, so let us take a look at a simple problem. Consider our first example of class X
above. We have two pointers to two different types of objects. Everything has gone well, and now the destructor of X
is invoked. It is pretty
X::~X() { delete p2_; delete p1_; }
Not much to go wrong here, but assume that it does say the T2
object throws an exception when deleted. Like constructors, destructors are special functions for the exception runtime mechanism. When an exception occurs in a destructor, it is treated like an exception in a constructor, i.e. all complete subobjects that still exist are destroyed (in reverse order of their construction) and then the memory deallocation function is called, if needed. (I assume this is what is suppose to happen. The Standard is not at all clear about how exceptions from destructors are handled. I make this assumption based upon the discussion of exceptions in the ARM (see ONMOUSEOVER = "self.status = 'Item E50'; return true" ONMOUSEOUT = "self.status = self.defaultStatus">Item E50), and because it seems the logical thing to do. It would not surprise me if compiler implementers
Exactly the same problem occurs here as it does in a constructor. The exception from the destructor of T2
will terminate the body of the destructor without deleting the resource held by p1_
. The solutions are likewise similar to those applied to a constructor.For example, we can attempt to catch the exception and guarantee that other resources are deleted. Unlike the constructor however, there is no way to organize things so we can cover everything in a single catch clause at least not without more work than is worth it. Once again, we turn back to the "acquisition is initialization" strategy and use auto_ptr
objects. If all our member variables are auto_ptr
objects then our destructor is empty anyway, so let us assume
X::~X() { auto_ptrt1(p1_); auto_ptr t2(p2_); }
This may seem a little silly, but it does work. We transfer ownership of the resources to the two temporary objects (we create them in the same order that p1_
and p2_
are declared so they will be destroyed in reverse order). When the destructor body exits, these objects are destroyed, deleting their pointers. If an exception occurs in the destruction of t2
, then the stack unwind will still destroy t1
.
Goal V. Do not catch
any exception you do not have to. There is an old C rule of error handling (actually it probably goes back to the Countess Ada Lovelace) that states: "do not test for any error condition you do not know how to handle." Like most cynical proverbs, there is a certain amount of wisdom in this. In C++, we now have a standard way to handle any error condition throw an exception but this just shifts the burden of the problem. The basic truth remains: it is a waste of time (yours and the computer's) to catch an exception you do not know how to
Guideline 7: Rewrite functions to preserve state, if possible. As we have seen in the discussions above, when an exception propagates upward from a lower level function, we may have to catch it just to reset the state of our object. The point of Goal V is that we want to avoid this as much as we can. Consider our rewritten push()
function. The last two lines now
v_[top_] = element; top_++;
We deliberately moved the incrementing of top_
after the possible exception from the assignment operator. We could have written something
try { v_[top_++] = element; } catch (...) { top_; // reset state throw; }
This is similar to what had to be done in the pop()
function. In that case we did not have any choice; in push()
we do. Goal V says that if we can arrange our functions so that we can avoid writing try/catch
blocks then we should do so. It leaves us with cleaner code and it is more efficient to let the exception just propagate than it is to catch and rethrow
There is a potential downside with doing this that has to be noted when we write a try/catch
block, it is obvious that there is the possibility of an exception. When we
v_[top_] = element; top_++;
v_[top_++] = element;
it is not clear that the first form is necessitated by the possibility of an exception. We run the risk that during maintenance, some experienced C programmer (but new C++ programmer) will take a look at the two lines and decide that whoever wrote them did not appreciate the compactness of expression possible in C++ and change them back into the latter. In some sense, this is one of the most annoying drawbacks to using exceptions in C++ a great many of the cherished idioms from C are going to have to be discarded. Instead, we are going to have to use a much more deliberate (and verbose) style that allows us to maintain better control over the state of our objects in the presence of
In order to avoid this maintenance problem, we must document every place in our code where we think an exception is possible, and especially everywhere we have changed code to make it exception safe. I use comments of the form //>x
. This is not a particularly good solution, but it is better than
Guideline 8. Always use a catch(...)
block to cope with propagating exceptions. If we follow Guideline 7 we will avoid catching exceptions if we can, but there will be times when it can not be avoided, or the work necessary to avoid it is excessive. If we conclude that an exception might occur, and that it could leave us in a bad state, then we want to make sure we catch that exception. Since we are not concerned with handling the exception, only with protecting the state of the object, then the type of the exception does not matter. Therefore we use a catch block with a single
catch (...) { /* ... */ }
This is pretty obvious for a template class like Stack
, where we have no idea what kind of exception might be thrown by T
, but in general, even if we know (or think we know) what the actual exception type is, if we are not actually handling the exception, then we should use a catch
(...)
Guideline 9. If you get stuck, call terminate()
. This is not really about handling exceptions, but it does have something to do with coping with errors. There are places where throwing an exception does not make sense, and the only possibility left is to end the program. An obvious circumstance where this happens is in a user defined version of unexpected_handler
. Unexpected_handler
must either throw an acceptable exception, or end the
Before exceptions, the normal way to abnormally terminate a program was by calling the C library function abort()
. In the new C++ world, we should call terminate()
instead. terminate()
just calls terminate_handler
. In the default case, this calls abort()
, but the user can replace the default terminate_handler
with a program specific version. So call terminate()
to allow any user defined terminate_handler
to
Goal VI. Do not hide exception information from other parts of the program that might need it. Just to reiterate the obvious: the purpose of exceptions is to pass information from the point where an error is detectable to a point where the error can be handled. If you throw a different exception rather than rethrowing the original exception, you want to make sure you are increasing the information content by doing
Guideline 10. Always rethrow the exception caught in a catch
(...)
clause. I will predict that this will probably be the most common exception handling mistake. Once you have reset your state in a catch
(...)
clause, you want to be sure that you rethrow the exception so that higher level routines will have a chance to handle it. A failure to rethrow the exception means you have "handled" the exception which is probably not what happened at all. Just for a reminder, the statement to rethrow an exception
throw; // see ONMOUSEOVER = "self.status = 'Item M12'; return true" ONMOUSEOUT = "self.status = self.defaultStatus">Item M12 for details
Guideline 11. Rethrow a different exception only to provide additional information or capability. If forgetting to rethrow the exception from a catch
(...)
clause is likely to be the most common exception handling mistake, rethrowing a different exception will probably be second. Consider the
void foo() try { // do something } catch (PrivateExceptionType& ex) { // clean up } catch (...) { // clean up throw exception("Unknown exception in foo"); }
At first reading this seems to make sense. Foo()
invokes some operation that might throw a private exception type, which is caught and handled. To be on the safe side, foo()
includes a catch-all block just to cover the bases. The problem is that when foo()
throws the generic exception in the catch-all block, it effectively destroys all the information in the original exception. This information presumably indicated what the original error was and might have been of use to a higher level routine that was willing and prepared to deal with an error of that type. Now, all that propagates upward is a self-fulfilling prophecy, and the program will probably terminate because nobody knows how to handle an "unknown
In our ongoing Stack
example, there are several places where we use the new statement. Under the C++ Standard, new will throw a bad_alloc
exception if the memory allocation fails. In the second part of this series of articles [Reference 3], we add a private exception class to Stack
that is derived from bad_alloc
. In Stack<T>::AllocationError
we include some additional information besides that available from bad_alloc
alone, including the new size of the stack being
In order to be able to throw Stack<T>::AllocationError
we use the nothrow()
placement form of new
(see ONMOUSEOVER = "self.status = 'Item E7'; return true" ONMOUSEOUT = "self.status = self.defaultStatus">Item E7) as
new_buffer = new (nothrow()) T[new_nelems]; if (new_buffer == null) throw Stack::AllocationError(new_nelems);
Class nothrow
is defined in the Standard header file <new>
along with the placement form of operator
new
used above. When this form is used, operator
new
reverts to the traditional behavior and returns a null pointer if the allocation fails. This is the preferred way to do this. Note that this form only eliminates the possible exception from operator
new
, an exception can still be thrown by the constructor of the object in the new expression. For that reason, we want to be careful NOT to do the
try { new_buffer = new T[new_nelems]; } catch (...) { // catch bad_alloc from new throw Stack<T>::AllocationError(new_nelems); }
In this case, our catch
(...)
clause will not only catch the bad_alloc
exception as the (erroneous) comment indicates, but we will also catch any exceptions thrown by the T::T() constructor
. If the new expression fails because of an exception in the latter, we will hide that exception with our version of bad_alloc
. While it is true that either case will indicate that the function failed, hiding the original exception means that any higher level routine that handles the Stack<T>::AllocationError
exception will be solving the wrong
Guideline 12. Make sure one catch block does not hide another. When the exception runtime attempts to locate a handler for an exception, it tries the handlers in order. Unlike function overloading, where the best match from a set of possible functions is found, an exception handler is found on a first match basis (see ONMOUSEOVER = "self.status = 'Item M12'; return true" ONMOUSEOUT = "self.status = self.defaultStatus">Item M12). This means that you always want to put more specific handlers before more general handlers. Handlers specifying base classes (or references to same) must come after those for more derived classes. Obviously, any catch
(...)
clause must be the last handler of a sequence. The compiler will almost certainly complain if the catch
(...)
clause is not the last one, but it may not generate an error message for other incorrect orderings, so
Goal VII. Never depend upon destructors for functionality in any situation where fault-tolerance is required. As a general rule, we do not want exceptions propagating from a destructor (see ONMOUSEOVER = "self.status = 'Item M11'; return true" ONMOUSEOUT = "self.status = self.defaultStatus">Item M11). There are two reasons for this. First, there is simply a semantic problem with deciding what it means for a destructor to have an exception. For ordinary functions, an exception typically means the function failed. What does it mean when a destructor fails? Certainly, it should not mean that the object is left in a usable state since it was not going to be usable if the destructor succeeded. In most cases the memory for the object will disappear anyway, whether the destructor succeeds or not. In other words, there is not much we can do about handling an error that occurs in a destructor, and we ordinarily want the destructor to finish anyway, so there is not much point in signaling errors from a destructor, even though exceptions give us a way to do
The second reason for avoiding exceptions in destructors is more fundamental - they are likely to abort the program. In a sense, destructors are part of the exception handling mechanism itself. Destructors are invoked by the exception handling runtime as part of the stack unwind process. If a destructor throws an exception during an exception stack unwind, then the Standard says the exception handling mechanism gives up and calls terminate()
. For this reason, other writers have taken the position that destructors should never throw or propagate exceptions [Reference 2, Reference 5]. I do not go quite that far, but the following guidelines should apply under most
Guideline 13. Do not throw exceptions from a destructor body. This is the principal behind Goal V again. Clearly, there is no sense in testing for error conditions in a destructor body unless they can be handled right then and there otherwise ignore them (or call terminate()
Guideline 14. Do not arbitrarily handle all exceptions propagating from a destructor. Exceptions can still legitimately occur when a destructor calls an ordinary function that throws an exception. It is tempting to suggest that destructors should not call such functions, or to say that every destructor should handle its own exceptions tempting, but unrealistic. The first is impractical, and the second leads to constructs such
catch (...) {}
which I object to on aesthetic grounds. On the other hand, are there ever legitimate occasions when we might want to propagate an exception from a destructor? Consider the following real-world
A program captures medical images from a Computed Radiography (CR) scanner and transfers them over a fiber optic network to a central image server.If there is any problem with the network, or with the image server, the program is required to save the image to local disk, generate a message on both the local console and the system administrator's console (a separate network is available for system administration tasks), and enter an error state that prevents any more images from being captured until the problem is fixed and the image saved on disk is transferred to the central server. This is necessary because the X-ray plates used by a CR machine are erased by the scan once scanned, the image must be captured or it is lost forever. (This program exists, and is written in C++. It was written before exceptions were available, and I doubt it has been updated. The process of doing so would make an interesting test case (see Reference 6).
Assume the network connection is encapsulated as an object (call it an endpoint). If the endpoint destructor is called, and the connection is still open, the destructor will attempt to close it. If some error occurs it seems reasonable, given the nature of this program, that an exception should be thrown to indicate a network problem to higher
This would seem like a perfect case for having a destructor throw an exception, but maybe not. A closer look at this program reveals that under normal circumstances the network connection should always be closed before the endpoint is destroyed. If the connection is still open when the destructor is invoked, it probably means the endpoint is being destroyed as a result of some other exception. If this is the case, then the last thing we want to do is throw another exception. In fact, we may be trying to destroy the endpoint as a result of a stack unwind that resulted from an exception thrown by another network function call. We want this exception to
So back to the question of whether a destructor should handle all possible exceptions or allow them to propagate. Destructors being the type of functions they are, it may make sense to ignore a lot of errors in destructors (i.e. catch the exceptions and not rethrow them). Nevertheless, at some point in coding a class or library, we have to acknowledge that our users will probably have a better view of the big picture than we do. There has to be a limit on how much we try to handle at a given level. This is especially true for destructors. The person who uses our classes has to accept some responsibility for how they are used. This means that any program that has to be fault tolerant must be designed and coded with special care regarding the types of exceptions it has to guard
If our medical imaging program is written correctly, then it will not depend upon destructors for normal functionality. The remote file will always be closed before the file object is destroyed; the network connection will always be closed before the network object is destroyed, etc. Exceptions are possible in fact the program may depend upon them but their occurrence will be anticipated as part of normal design, and they will be handled outside of the destructors. We are left with something of a paradox: the more fault tolerant a program has to be, the less likely it is going to have to worry about exceptions from
As noted above, I object to constructs such
catch (...) {}
on aesthetic grounds alone, but there is a practical aspect as well. Templates are but one example of a case where we have no idea what kinds of exceptions are possible from the operations being invoked on the objects used to instantiate the template. There are times when we have to assume that the user knows what she is doing and stay out of the way. This goes even for exceptions propagating from
Goal VIII. Do not get too paranoid. As a final point, I have noted that it is possible to get paranoid when trying to deal with exceptions. Maybe this is only a problem for me because I have spent so much time lately worrying about exceptions. Still, I am willing to bet that other people are also going to run into
When you start trying to deal with every possible exception that might occur, you can get into a situation where you either have to assume that an operation will work, or call terminate()
. Consider what happens to our Stack if we try to make sure that an exception in push()
always leaves the Stack in a good state. As we discussed under Guideline 3, if the assignment in the final step of push
fails, and T::operator=()
does not leave the element at v_[top_]
in a good state, then our Stack is not in a good state. We might try to guarantee that the Stack
is always left in a good state like
try { v_[top_] = element; top++; } catch (...) { v_[top_].~T(); // destroy the 'bad' object new (&v_[top_]) T(); // create new 'good' object throw; }
If the constructor in the catch clause throws an exception, then we have left our Stack
object in an undefined state. After destroying the object at v_[top_]
we can not even expect the destructor for Stack
to work. We must assume that the constructor will be able to reinitialize the object at v_[top_]
.
If you find yourself writing code like this, take it out. As we discussed in the beginning, exception handling is something that must pervade the entire program. At some point you have to turn the problem over to the user. Ultimately, only the application developer is in a position to really decide how to deal with certain errors. More often than not, the application will have to seek help from a human operator. In the final analysis, often the best we can do in handling an exception is to make sure our software stays together, and then propagate the exception and let a higher level deal with
As shown in this article, exceptions raise many issues that either were not there before, or which had much simpler solutions. Many of these Goals and Guidelines can be illustrated with simple examples, but are not simple to apply in real
The good thing about exceptions is that not every program needs to be truly fault tolerant in fact, very few do. What is much more important is that a program be robust. A robust program is resistant to errors it either works correctly, or it does not work at all; whereas a fault tolerant program must actually recover from errors. There are obviously different levels of "robustness." For years, I have liberally sprinkled my code with "assert" statements to make them more robust. Since all an assert statement usually does is print a message and abort the program, there are those who might question whether this actually qualifies as an improvement in robustness. Certainly, this is not acceptable behavior in a shipping application. For this reason, assert statements are usually used only for debugging and disabled in the final build of an
Before exceptions, the only alternative to a deliberate abort was to return error flags and check them religiously. Exceptions provide a third possibility. If an exception is not handled at some point in a program, it will propagate out of function main()
and invoke terminate()
. Thus, an un-handled exception has much the same effect as an assert statement. Conversely, the program can choose to handle the exception (if only to provide a more graceful exit). For this reason alone, programmers are going to insist upon throwing exceptions, especially in library code it relieves them of the problem of deciding whether to abort the program, or not. As noted in the beginning, throwing exceptions is easy, coping with them is
The simplest way to start out coping with exceptions is to have a single try/catch
block in the main()
function to provide a meaningful error message and a graceful exit of the program (Goal V applied to the entire program). A program written this way may not be any more robust than a properly written C program, but that level of robustness will have been obtained with a lot less effort (virtually none), and a lot less code. Just think of all the if statements that will not have to be
From that point, developers can start to actually apply these guidelines to libraries and functions in order to bring them up to a point where they can be used in a fault tolerant program. We have to be careful not to rush into using catch clauses to handle exceptions or we run the risk of falling back into the problem where we think the program is running fine but in fact it has entered an erroneous state. Nevertheless, when used correctly, exceptions can definitely improve the reliability of our programs. As the examples in this paper have shown, this may take a lot of work, but developing reliable software usually