Home : C++ : Exceptions

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:

I said above that one function in the initialiser list may throw an exception. The reason for it being called last is so that:

  1. If you want to rethrow, you know all other members are initialised when the destructor is called.
  2. 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:

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.

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.

Comments

Members have left 0 comments about this page:
Please Login or Register to comment on this page.

Resources
Tools
User
Last Updated Wednesday, 24-May-2006 22:39:16 BST