Exception Safety
Writing code that may be interrupted by an exception is not
easy and takes practice. Even the simplest code must be thought
through to insure that it works properly in the event that
an exception is thrown while it executes ("exception safe"). This is especially true when writing
member functions of an object that change the object
(mutators, assignment operator and constructors
-- we need to insure that the object does not become a "zombie".
A Smart Array Example
Consider the class below that implements a variable size
array of dynamically allocated Fred objects.
class FredArray
{
public:
// constructors
FredArray ( int size = 100 );
// assignment operator
FredArray operator=( const FredArray& rhs);
// other public methods
private:
int m_size; // the number of Freds in the array
Fred *m_data; // a pointer to the array of Freds
};
// unsafe assignment operator implementation
FredArray& FredArray::operator=( const FredArray& rhs)
{
if ( this != &rhs )
{
// free existing Freds
delete [] m_data; // 1
// now make a deep copy of the right-hand object
m_size = rhs.m_size; // 2
m_data = new Fred [ m_size ]; // 3
for (int j = 0; j < m_size; j++ ) // 4
m_data[ j ] = rhs.m_data [ j ]; // 5
}
return *this;
}
What's the problem with relatively simple this code? It's not
"exception safe". Code which is exception-safe guards against leaving
an object (or program) in an invalid or inconsistent state if an
exception should occur. Let's examine the code line by line
- Line 1 -- delete[]
comes with a guarantee not to throw an exception.
- Line 2 -- obviously no exception, but this line changes the
state of the FredArray
- Line 3 -- although unlikely, may throw an exception
- Line 4 -- no problem
- Line 5 -- invokes Fred's assignment operator
Now consider the state of the FredArray object if either line 3
or line 5 throws an exception. The old array has been deleted
(line 1), the size has been changed (line 2),
but not all of the Freds have been copied.
We address these issues by carefully writing the code so that any exception
that might be thrown does not cause the FredArray to take on an invalid
state. In Herb Sutter's book Exceptional C++, he puts it something
like this - first do anything that would cause an exception "off to the
side" using local variables so as not to change the state. When the
"real work" has been accomplished successfully, then use
operations that don't throw exceptions to actually change your program/object
state.
// Better assignment operator implementation
FredArray& FredArray::operator=( const FredArray& rhs)
{
if ( this != &rhs )
{
// code that may throw an exception first
// make a local temporary deep copy
Fred *tempArray = new Fred[rhs.m_size];
for ( int j = 0; j < rhs.m_size; j++ )
tempArray[ j ] = rhs.m_data [ j ];
// now code that does not throw exceptions
delete [] m_data;
m_size = rhs.m_size;
m_data = tempArray;
}
return *this;
}
Exception Guarantees
There are three generally accepted classifications of exception-safe
guarantees that a function can make in increasing order of
"strictness". We should make sure that our
code falls into one of these categories.
- Weak (Basic) Guarantee -- the function ensures that the program/object
does not become corrupt. No resource (memory) is leaked and the object
remains in a usable, destructible state.
- Strong Guarantee -- the function ensures that if an exception is
thrown, the function has no effect. The program/object state is the
same as it was before the function was thrown.
- NoThrow Guarantee -- the function ensures that it will not throw
an exception under any circumstances. Destructors, delete and
delete[ ] should provide the NoThrow Guarantee.
It's sometimes said that the Basic/Weak and Strong guarantees represent the
two levels of exception safety. The purpose of the NoThrow guarantee is to
enable the Basic/Weak and Strong guarantees to be honored.
Which guarantee should our code support? The C++ standard library
follows this guideline
A function should always support the strictest guarantee that it can
support without penalizing users who don't need it
What do we mean by "penalizing" your user? Adding additional overhead
in terms of execution time and/or space. Providing the Strong Guarantee
often (not always) requires a trade-off in performance.
For example, consider a function that inserts a item into the middle
of an array or vector. To provide the Strong Guarantee, this code
would have to make a copy of the original array/vector before attempting
the insertion, a clear impact on the performance of the code.
Dennis Frey
Last modified: Fri Nov 19 11:54:46 EST 2004