The hard part about writing exception safe code isn't the throwing or catching of exceptions; it's everything in between. As a thrown exception wends its way from the throw expression to the catch clause, every partially executed function on that path must "clean up" any important resources that it controls before its activation record is popped off the execution stack. Generally (but not always), all that is required to write an exception safe function is a moment's reflection and some common sense.
For example, consider the implementation of String assignment from Assignment and Initialization Are Different [12, 41]:
String &String::operator =( const char *str ) { if( !str ) str = ""; char *tmp = strcpy( new char[ strlen(str)+1 ], str ); delete [] s_; s_ = tmp; return *this; }
The implementation of this function may look superfluously ornate, since we could have coded it with fewer lines and no temporary:
String &String::operator =( const char *str ) {
delete [] s_;
if( !str ) str = "";
s_ = strcpy( new char[ strlen(str)+1 ], str );
return *this;
}
However, while the array delete comes with a social guarantee not to throw an exception (see Exception Safety Axioms [38, 131]), the array new a couple of lines later makes no such promise. If we delete the old buffer before we know whether allocation of the new buffer will succeed, we'll leave the String object in a bad state. Herb Sutter summarizes the situation well in his Exceptional C++, which I'll paraphrase as this: First do anything that could cause an exception "off to the side" without changing important state, and then use operations that can't throw an exception to finish up. That's what we did in the first implementation of String::operator = above. Let's look at another example from Commands and Hollywood [19, 67]:
void Button::setAction( const Action *newAction ) { Action *temp = newAction->clone(); // off to the side... delete action_; // then change state! action_ = temp; }
Because it's a virtual function, we really know nothing about the exception-related behavior of the call to clone, so we assume the worst. If the clone operation succeeds, we continue with an exception safe deletion and pointer assignment. Otherwise, a thrown exception from clone will cause premature exit from Button::setAction with no harm done. Newer C++ programmers have a tendency to "clean up" code like this in such a way as to make it exception unsafe:
void Button::setAction( const Action *newAction ) { delete action_; // change state! action_ = newAction->clone(); // then maybe throw? }
Performing the deletion (which is assumed to be exception safe) before the clone (which makes no such promise) will leave the Button object in an inconsistent state if clone throws an exception.
Notice that properly written exception safe code employs relatively few try blocks. A novice attempt to write exception safe code is often littered with unnecessary and often damaging trys and catches:
void Button::setAction( const Action *newAction ) { delete action_; try { action_ = newAction->clone(); } catch( ... ) { action_ = 0; throw; } }
This version with its fussy try block and catch clause is exception safe in the sense that the Button object is left in a consistent (but likely different) state if clone throws an exception. However, our previous version was shorter, simpler, and more exception safe because it left the Button object not merely consistent but unchanged.
It's a good idea to minimize the use of try blocks, where possible, and employ them primarily in locations where you really want to examine the type of a passing exception in order to do something with it. In practice, these locations are often at module boundaries between your code and third-party libraries and between your code and the operating system.