mr-edd.co.uk :: horsing around with the C++ programming language

Tips for designing exception classes

[28th June 2008]

Creating good exception classes is a reasonably tricky affair. By definition, exceptions generally aren't expected and so aren't thrown that often. But the whole point of exceptions as opposed to error codes is that you get to choose how far up the call-stack to handle the error, rather than having to deal with every error close to the source. This means that you should aim to make your exceptions rich so the catcher is in a better position to decide if and how to handle the exceptional condition.

So here are 9 tips for designing good exception classes. I tried to make it 10, but couldn't eek out another one!

1. Inherit an exception class defined by the standard library

Many programs rely on exceptions being derived from a standard library exceptions. In main(), it's common to attempt to catch a const std::exception & as a last-ditch attempt.

But that's not to say that you should inherit directly from std::exception. It's often better to use something deeper in the hierarchy. Perhaps std::logic_error or std::bad_cast is more appropriate for your particular exception.

The layers in exception hierarchies are used to bundle together similar or related error causes so that they can be handled uniformly. This is a long-winded way of saying that you should embrace the polymorphism that the exception handling mecahnism — i.e. catch() — gives you.

While we're on the subject of inheriting standard library exceptions, one mistake that I sometimes make absentmindedly is to override std::exception::what() without adding the const specifier:

class my_exception : public std::runtime_error
{
    public:
        ~my_exception() throw(); // throw() needed to prevent compiler error
        const char *what() const throw(); // throw() needed to prevent compiler error
        //                 ^^^^^

        // ...
};

If you forget the const highlighted in the above snippet, you won't really be overriding the what() defined in the base class! Some compilers will warn you when you do this. Others won't.

Also, you need to make sure that you get the exception specification correct for each member function you override. In other words, if you derive from a standard library exception, make sure that the overridden functions declare themselves incapable of throwing anything, as I've done for what() and the destructor, above. Ideally, you wouldn't have to do this as exception specifications are generally considered a bad idea, but the language says you must match those when overriding a virtual function.

2. Don't rely on strings to convey the exceptional condition

The standard library provides a what() member function that returns a textual representation for the cause of the exception. I think it's a mistake for a number of reasons.

First of all, who is the string for? The application user or a developer? Either way, will the string be the best explanation in all circumstances? (Ans: probably not).

Secondly, in what language is the string? English? Your native tongue? Esperanto? There's no right answer. You could of course provide a translation for all known languages (yeah, right!), but that would be overkill. Besides, applications differ on their multilingual support mechanisms so the one you might provide for your exception class may not gel well with that of the client code.

Thirdly, error messages can change over time. If a client relies on the format of your exception's message being stable in order to parse certain details out of it, they'll be in for a nasty surprise when it eventually changes.

However, this tip is somewhat in conflict with my first tip; if you inherit from a standard library exception, you have a what() method to override. So my advice would be to override what() anyway as it is useful for debugging if nothing else, but do make sure to provide another way to determine the meaning of the exception.

3. Use the exception's type as a differentiator

Let's say you were designing a library that had to open files as part of its function. Opening a file can fail for many reasons, for example:

So you might be tempted to create an exception that looks something like this:

class file_open_error : public std::runtime_error
{
    public:
        enum failure
        {
            file_not_found,
            opened_exclusively_elsewhere,
            hardware_failure,
            // etc...
        };

        file_open_error(const std::string &filename, failure reason);
        ~file_open_error() throw();
        const char *what() const throw();

        failure reason() const;
        std::string filename() const;

    // ...
};

Note here, that as per the advice given in tip #2, I've already provided a means to determine the reason for the exception other than by examining the result of what(). But now let's look at what client code has to do to handle a hardware failure:

try
{
    // code that might throw a file_open_error
}
catch (const file_open_error &err)
{
    if (err.reason() == file_open_error::hardware_failure)
    {
        // handle the error
        // ...
    }
    else throw; // we don't know how to handle the exception here, so re-throw it
}

If we instead provided some extra classes, file_not_found, opened_exlusively_elsewhere and hardware_failure, each derived from file_open_failure, it makes things much easier to identify the exception in client code:

class hardware_failure : public file_open_error
{
    // ...
};

try
{
    // code that might throw a file_open_error
}
catch (const hardware_failure &err)
{
    // handle the error
    // ...
}

The amount of scaffolding needed in client code has been reduced. Of course, you could keep the failure enum in the file_open_error base class, but the derived classes should be provided as well.

Another benefit gained from deriving exceptions is that you can add additional information in each derived exception. For example, the openeded_exlusively_elsewhere class might be able to contain information relating to the application that has control of the file. You couldn't put this extra information in to a generic file_open_error class with a clear conscience.

4. Exceptions need to be copied, too

When an exception is thrown, it may need to be copied. If the compiler generated copy constructor and assignment operator aren't sufficient, make sure to define them appropriately. If your program crashes due to a thrown exception's copy constructor causing a segmentation fault, the stack trace will probably make you cross-eyed.

It's unusual to define copy constructors and assignment operators for polymorphic types, but in this case it's needed. Of course, if you do need to define one of these functions, you probably also need to define the other and a custom destructor too[1].

5. Don't rely on exception destructors to clean up resources

If an exception escapes your program uncaught then the destructor may not be called. This means that you shouldn't rely on the destructor of your exception classes to clean up resources that the OS can't clean up itself when the process is spent.

For example, on UNIX[2], System V shared memory will not be reclaimed by the OS if your program does not release it explicitly. So don't put a boost::shared_ptr<void *> member object referring to some shared memory in your exception class. The memory may not be released as the exception destructor may not be called.

All operating systems worth their salt will close things such as files and release allocated memory, so don't worry about strings, arrays and such like. But take care with more peculiar resources.

6. Pack as much information in to an exception object as possible

As we saw in an earlier tip, by deriving new exception classes we supply more information to client code just by virtue of having a more specific type to catch. This is a good start and arguably the most important thing we can do to make handling the exception easier.

But we should aim to go further where possible. Exceptions are often translated in to user-visible diagnostics or other exceptions. For this reason, pack as much information in to the exception object as you can so that a high quality description can be given to the user, or translation in to another kind of exception can be performed accurately.

7. Don't be afraid of big member objects

If you're anything like me (and it's quite possible/fortunate that you're not) you'll tend to assume that adding expensive members to exception objects isn't a good idea. I'm talking STL containers and other objects that might be expensive to copy. But if you think about it, there's really no reason to worry about this kind of thing.

One concern is that creating or copying such an object might result in a std::bad_alloc exception getting thrown. This is true, but it's likely that this would have happened soon anyway if your application is that low on memory.

Another worry is that copying big objects such as containers is an expensive thing to do. This is true, but exceptions are exceptional — they happen infrequently so the amortized cost is very small. When this isn't true, you're probably throwing exceptions in tight loops. In this case I'd guess that you're probably abusing exceptions, anyway.

8. Consider providing a base class or mixin if you're designing a library

This tip is related to #3. If you're writing a library, consider deriving all exceptions that your library may throw from a common base class. This allows clients to easily determine where the exception came from and therefore selectively deal with or ignore those exceptions.

For example, let's suppose that your library derives a number of exceptions from std::runtime_error. If the client wants to deal with all std::runtime_error exceptions except those that originate from your library, they'll have to write something like:

try
{
    /* some code that calls in to your library */
}
catch (const library_exception1 &ex)
{
    throw;
}
catch (const library_exception2 &ex)
{
    throw;
}
catch (const library_exception3 &ex)
{
    throw;
}
catch (const std::runtime_error &ex)
{
    // handle the runtime_error
    // ...
}

If you provided a mixin, then this would be simplified:

try
{
    /* some code that calls in to your library */
}
catch (const library_mixin &ex)
{
    throw;
}
catch (const std::runtime_error &ex)
{
    // handle the runtime_error
    // ...
}

The reverse situation also arises if your library derives its exceptions from different standard library exceptions. Perhaps library_exception1 inherits std::logic_error but library_exception2 inherits std::invalid_argument. In this case how can client code easily single out exceptions that have been thrown by your library if they do not derive from a common base?

9. A helping hand for multithreaded code

C++ doesn't officially support threads yet, though in the real world many C++ applications are threaded. What happens when an exception is thrown in a thread is entirely up to the system on which the code is running. One of the most sensible things you can do though, I think, is attempt to store the exception somewhere and throw it when the thread joins with its parent.

In general, this is hard to do. In order to save an exception object, we need to catch it by its static type rather than its dynamic type else we might end up slicing the exception when we save a copy. For example:

void thread::run()
{
    // called in the child thread
    try
    {
        body();
    }
    catch (const std::exception &ex)
    {
        // copy ex so that we can re-throw it when we join with the parent
        save(ex); // may slice!!
    }
}

void thread::join()
{
    // called in the parent thread
    wait_for_child();

    if (exception_saved())
        throw_saved_exception();
}

In the sketch of thread::run() provided above, we catch any std::exception thrown by the thread's body and copy it so that we can re-throw it later on. However, when we perform the copy, we don't know the full type of the exception.

We could add more catch() blocks for other exception types, but in general we can't predict the kinds of exception that a thread body will throw. It's impossible for the author of the thread class to be able to predict all the exception types thrown from arbitrary thread bodies, so instead we can provide a mechanism to enable them to store a copy without slicing.

To make your exceptions easier to propagate to parent threads, you should consider inheriting from a mixing that allows:

  1. your exception to be cloned polymorphically
  2. and a clone of your exception to be re-thrown

Such a mixin might look something like:

class rethrowable
{
    public:
        virtual ~rethrowable();
        virtual std::auto_ptr<rethrowable> clone() = 0; // polymorphic copy
        virtual void throw_copy() = 0;
};

Now all exceptions derived from rethrowable can be stored so that they may be re-thrown in a parent thread:

try
{
    // ...
}
catch (const rethrowable &ex)
{
    save(ex.clone()); // can call the throw_copy() member later on
}

10. There is no 10

Yeah, as I said, I could only think of 9! If you have any others, feel free to leave a comment!

Footnotes
  1. as per the rule of three []
  2. as far as I know, this holds for all Unices []

Comments

lsalamon

[04/07/2008 at 03:02:00]

Edd

[04/07/2008 at 19:20:00]

Hi Isalamon,

My opinions are always right ;)

I guess you mean this part:
http://google-styleguide.googlecode.com/svn/trunk/cppguide.xml?showone=Exceptions#Exceptions
?

I personally don't have any of the problems described in their list of cons. The thing is, all the languages Google use internally have standard libraries that throw exceptions. The only difference in C++ is that you have to write the code to deal with exceptions in a destructor (RAII), rather than in a finally block (Java) or an __exit__ method (Python).

I'm sure Google don't shirk the use of exceptions in these other languages. The concept of exception safety isn't exclusive to C++. Besides, if you write tests religiously, as Google do, you should have little or nothing to worry about.

Anecdotally, I've found that accepting the existence of exceptions makes you write better code. For example, you often end up with objects having a small non-throwing swap() method, which is extremely useful for minimising the amount time one needs to hold a lock when moving state around. You also end up with a much better mapping of resources to objects and therefore a clearer definition of ownership.

(optional)
(optional)
(required, hint)

Links can be added like [this one -> http://www.mr-edd.co.uk], to my homepage.
Phrases and blocks of code can be enclosed in {{{triple braces}}}.
Any HTML markup will be escaped.