Exceptions
Exception safety is one of the most often overlooked aspects of C++ programming and yet its when your users need your help the most that exceptions will fire - out of memory or disk full problems, for example.
Exceptions are good, they are your friend, grow to love them and you will be rewarded with an easier life. I can say this because I know.
Advantages of Using Exceptions
In the olden days, when programs were crafted from C and fire, a function would indicate success or failure by returning a value which always had to be checked. This led to loads of indentation and McCabe's metrics because a typical function would look like this:
void DoSomethingUseful() {
int rc = SUCCESS;
if((rc = DoSomething()) == FAILURE)
HandleError(rc);
else
if((rc = DoSomethingElse()) == FAILURE)
HandleError(rc);
else
if((rc = MyGodWillThisIndentationNeverEnd()) == FAILURE)
HandleError(rc);
}
The code snippet above only does three things and yet manages to be difficult to read and annoying to write. The error handling mechanism is the same for every function, but it appears three times - what a waste.
A further advantage is that your exceptions can be much more intelligent than mere error codes. If you throw an object in response to a failure, the object can contain a URL explaining to the user why the problem occurred and how to fix it. Exception class hierarchies are also very useful, reporting a file permission problem can be allowed to use more resources than reporting an out of memory error, yet both exceptions should present the same interface.
Handling Exceptions Correctly Inside Functions
As exceptions unwind the stack, resource leaks can occur if they are not handled properly. Consider the following code:
void DoSomethingUseful() {
X *pPointer1 = new X;
Y *pPointer2 = new Y;
// Do something here
delete pPointer2;
delete pPointer1;
}
Either allocation can throw an out of memory exception but if the first succeeds whilst the second fails, the memory will be leaked. Therefore we must catch the exception even we do not ultimately want to handle the error condition here - we can always re-throw the exception. The correct code is shown below:
void DoSomethingUseful() {
X *pPointer1 = new X;
try {
Y *pPointer2 = new Y;
// Do something here
delete pPointer2;
} catch(...) {
delete pPointer2;
throw;
}
delete pPointer1;
}
This example loses much of the readability that I argued exceptions should provide above, indeed it is pretty confusing. However, help is at hand in the form of smart pointers and the auto_ptr provided by the STL. A much groovier version using these techniques follows:
void DoSomethingUseful() {
std::auto_ptr<X> pPointer1 = new X;
std::auto_ptr<Y> pPointer2 = new Y;
// Do something here
}
Exceptions In Constructors
One of the more esoteric problems of producing exception safe code, is how do you handle exceptions in constructors? The answer is very carefully.
Inside the curly braces of the constructor, you can proceed as normal, but if your class contains references or const members, you'll need to exercise caution with your initialiser list. Whilst the object's destructor will still be called when an exception leaves the constructor, this means you may end up attempting to destroy partially initialised objects. The rules to follow are as follows:
- If you must use functions (not including constructors) in your initialiser list, only allow one to throw an exception.
- If an exception is thrown from a function in your initialiser list, that entry must be called last.
- Only objects that have completed their constructor will be destroyed when the stack is unwound so member objects will be destroyed correctly if they were initialised correctly.
- Never, ever, use the new operator in an initialiser list.
- Remember, the order of construction is important - members are constructed in the order of their declaration, not the order of the initialiser list.
- Check whether the constructors of any base classes may throw exceptions.
I said above that one function in the initialiser list may throw an exception. The reason for it being called last is so that:
- If you want to rethrow, you know all other members are initialised when the destructor is called.
- If you want to continue using the object, you know all other members are initialised properly.
In order to handle an exception from an initialiser list, you need to use a function try block. They can be used for any function, but are only really necessary in constructors. The code below shows that the try-catch block lives outside the curly braces:
X *ReturnX();
class CmyClass {
X *m_pPointer1;
public:
CmyClass()
try {
: m_pPointer1(ReturnX()) {}
} catch(...) {
m_pPointer1 = NULL;
throw;
}
};
On the whole, I think you'll agree, its much safer never to allow exception in constructors.
Exceptions In Destructors
Just don't.
Ever.
In theory, an exception thrown from a destructor will cause the exit() function to terminate the
program. In practice, this strategy is flawed because exit() should tidy up any global/static
objects, calling their destructors, which may cause further exceptions. Most compilers
handle this scenario differently, some ignoring the exception, some propagating it up the stack.
In addition to this, you should ask yourself why a destructor is attempting to acquire resources.
So its best to prevent exceptions from ever being thrown from destructors.
Exception Strategies
There are a handful of published exception strategies, which are described below:
Resource Acquisition Through Initialisation
The most common exception strategies (with respect to C++ anyway) is that of "resource acquisition through initialisation" described by Bjarne Stroustroup in his book "The C++ Programming Language". This approach relies on each constructor acquiring a maximum of one dynamic resource. Objects which need the use of more than one resource should use other objects to acquire them and construct those objects within the initialiser list.
Whilst this is an excellent approach, it can lead to a class explosion, e.g. instead of single class being used to represent a file, you'll need classes for open files, locked files, memory mapped files etc. However, the approach has been used successfully by many commercial and freeware libraries.
Structured Exception Handling
Exceptions were first introduced in procedural languages, such as Ada83, and fitted very well with the top-down development approach favoured by these languages. This approach assumes that the business logic within a program is called in a hierarchical manner so that each level within the hierarchy cleans itself up and passes the problem upwards (kind of like politics really).
This approach may not be appropriate to all OO programs, especially those where asynchronous events are used to allow objects to communicate, or where finite state machines are involved.
Two Stage Initialisation
This is one of the easiest mechanisms to implement correctly. It specifies that constructors should never throw exceptions, but that a separate member function should be used to acquire resources after the constructor has completed. This member function can then follow the structured exception handling approach.
Common Exceptions
I've listed the common exceptions from C++, the STL & MFC below.
C++Exceptions
The C++ language defines a number of standard exceptions:
bad_cast- thrown by thedynamic_casttemplate when casting to an object of the incorrect type.bad_typeid- thrown by thetypeidfunction when it is passed a NULL pointer.
Note that, by default, the new operator does not throw an exception. It can be made to
by calling set_new_handler, see the
reference manual
for details. Use of the STL appears to contradict this rule, throwing std::bad_alloc
whenever allocation fails.
STL Exceptions
Exceptions are used widely throughout the STL although support is often implementation dependant.
std::bad_alloc- thrown by thenewoperator when memory allocation fails.std::bad_cast- as above.std::bad_typeid- as above.std::logic_error- base class forstd::domain_error,std::invalid_argument,std::length_errorandstd::out_of_range.std::domain_error- not sure what this is for.std::invalid_argument- does exactly what is says on the tin.std::length_error- thrown when container classes get too big, such as appending too many characters to a string.std::out_of_range- thrown by the container classes when accessing a value at the wrong index.std::ios_base::failure- thrown when an IO operation fails.std::runtime_error- base class forstd::range_error,std::overflow_errorandstd::underflow_error.std::range_error- thrown when internal range calculations fail - such as when using unrelated iterators.std::overflow_error- thrown when a numeric value cannot get any larger.std::underflow_error- thrown when a numeric value cannot get any smaller.
MFC Exceptions
Microsoft's MFC library defines a number of exception classes which are well documented in MSDN so I won't repeat them here.
A point worthy of note, is that Windows DLLs must be used to free any memory which they allocate, therefore
care should be taken when throwing exceptions across DLL boundaries. The MFC exception classes have
private destructors but a public function called Delete() to work around this problem in
a neat manner.
Good Practice
Writing exception safe code can be difficult but you can get some help from the compiler. Specifying exactly which exceptions (if any) are thrown from each function can help focus the author's mind on the task and also bring to their attention any uncaught exceptions at compile time.
Also, the catch all mechanism - catch(...) - should be used with care. It can be a cop out
for programmers who are too lazy to correctly analyse the exception safety of their code. Where a structured
exception handling approach is used, it is reasonable to catch all exceptions, release any resources
acquired in the current sub-program but then you must rethrow and allow the caller to handle the original
error.
Remember that exceptions are most likely when resources are low, therefore you should ensure that your exceptions do not need the resource that caused the exception in the first place. For instance, provide a global object representing an "out of memory" condition so it does not need to be allocated from the heap when the condition occurs.
When throwing objects, always throw references or pointers to the object to prevent the copy constructor being called too many times. You must then ensure that your object will not go out of scope because of the exception itself.
