Lesson #20

Standard Exception Types

Overview

In this lesson I will cover the following topics

  1. Exception hierarchies.

  2. The exception classes defined by Standard C++.

  3. Exceptions vs assertions.

Body

Exception hierarchies

Since it is possible to catch exceptions by reference, it is often desirable to define a hierarchy of related exception classes. To see why, consider the following example. Suppose a library of network functions defined a class NetworkError to use as a general purpose exception type. Users of the library might write

try {
    // Call the network library functions.
}
catch(NetworkError &bad_thing ) {
    std::cerr << bad_thing.get_message( ); << endl;
}

Here I'm assuming that the NetworkError class has a method named get_message that returns a message describing the error. Thus, the handler above simply prints out that message whenever it catches a network related exception. The exception object may contain additional information about the error and other methods for accessing that information.

The library might derive a number of different classes from NetworkError with each class representing a specific type of error condition. Here is how the network library might define these classes.

// Generic exception class.
class NetworkError {
private:
    const char *message;

public:
    NetworkError( const char *m ) : message( m ) { }
    virtual const char *get_message( ) { return message; }
};

// Specific type of network error.
class ConnectError : public NetworkError {
public:
    ConnectError( const char *m ) : NetworkError( m ) { }
};

// Another specific type of network error.
class TXError : public NetworkError {
public:
    TXError( const char *m ) : NetworkError( m ) { }
};

Now when the library code wants to throw an exception, it will create a suitable object, include an appropriate message, and send it on its way.

if( bad_thing_happened( ) ) {
  throw ConnectError(
    "Unable to connect. File: " __FILE__ "; Line: " __LINE__);
}

This example uses the __FILE__ and __LINE__ preprocessor macros to embed the source file name and line number into the message. This isn't always appropriate. Such information would only be confusing to the end user, but it might be quite useful to a developer. Exactly when it is reasonable to include such internal information in an error message is a topic of ongoing debate among programmers.

Notice that my example library code creates a ConnectError object to throw. However, since ConnectError is derived from NetworkError, the handler for NetworkError will catch this exception. When the application's exception handler calls get_message against the NetworkError reference, the message used to construct the ConnectError object will be displayed.

In general, serious libraries might define a large number of exceptions. However, if the application catches the base exception, the application does not need to be aware of all the individual exception types. Of course, the application is free to catch the specific types if it desires. For example, the application might do

try {
    // Call the network library functions.
}
catch( connect_error &bad_thing ) {
    // Handle the specific case of a connect_error.
}
catch( network_error &bad_thing ) {
    // Handle all other network errors.
}

The catch clauses are tried in order, and the first one found that matches the exception is used. Thus, in the example above, ConnectError exceptions are handled by the handler specifically for that type. Had I put the NetworkError handler first, however, the ConnectError handler would never get invoked. All ConnectError exceptions would be caught by the more general handler before noticing the specific handler. Many compilers will warn you if you set up your handlers in an illogical order.

The beauty of this method is that if a new version of the network library is released that defines additional exceptions, the application code does not necessarily need to be changed. The new exceptions, presumably derived from the same base class (NetworkError in this case) will be handled in a generic way without the application code even needing recompiling.

Standard exception classes

Although C++ does not require all classes to be derived from a single, ultimate base class (as does a number of other object-oriented languages), the C++ standard does provide a class hierarchy in the library intended for use in defining exceptions. The standard library itself uses classes from this hierarchy for its own exceptions. Some people believe it is best to create your own exception classes by deriving them from the standard hierarchy as well.

The advantage of using the standard exception class hierarchy as a base for your own exception classes is that programmers who use your code will already have some idea of the meaning and purpose of your exceptions. Also, application programs that catch the standard exception classes will automatically catch exceptions from your code in a natural way. Keep in mind, however, that the C++ language does not require that you use the standard library exception classes in any way. You are free to create your own exception hierarchy or even use non-class types such as int or long for exceptions. It is my recommendation, however, that you follow the lead set by the standard and use the library exception hierarchy either directly or as a base of your own exception types.

The library class that is the base of all library exception classes is std::exception. It is defined in the header <exception>. Exception objects have only one method named what that returns a pointer to a constant character. The string returned by what is intended to be a human-readable message. Thus:

try {
    // Do whatever
}
catch( exception &bad_thing ) {
    cerr << "Exception caught!: " << bad_thing.what( ) << endl;
}

The handler above will catch all library exceptions and all exceptions from non-library code that uses the standard exception classes.

Most of the standard exception classes are defined in the header <stdexcept>. I will show the full hierarchy below using abbreviated class definitions.

namespace std {

    // The two general exception types.
    class logic_error   : public exception {...};
    class runtime_error : public exception {...};

    // Specific logic errors defined by the standard.
    class domain_error     : public logic_error {...};
    class invalid_argument : public logic_error {...};
    class length_error     : public logic_error {...};
    class out_of_range     : public logic_error {...};

    // Specific runtime errors defined by the standard.
    class range_error     : public runtime_error {...};
    class overflow_error  : public runtime_error {...};
    class underflow_error : public runtime_error {...};
}

Each of these classes has a constructor that takes a reference to a constant std::string. The string given to the constructor is the same string returned from the what method. For example, the code that throws an exception might look like:

if( bad_program_state( ) )
    throw logic_error( "Internal inconsistency detected" );

The standard divides exceptions into two broad categories. The first, logic errors, are intended to represent problems with the program itself. Logic errors are errors that could have been avoided by careful programming. Throwing a logic_error exception is something to do when your program detects a bug in itself.

In contrast, a runtime error is an error due to something beyond the program's control. Run time errors could not be checked when the program is compiled, even in principle. They are due to the nature of the input given to the program or to the limitations of the environment in which the running program finds itself.

The standard then goes on to define several more specific exception classes of each general type. Here are some comments on each of those classes.

  1. class domain_error : public logic_error {...};

    Mathematical functions have a domain and a range. The domain is the set of legal values of the function's argument(s). For example, a square root function that returns double can't handle negative arguments because the square root of a negative number is imaginary. One would say that a negative argument is "outside the domain of the function." Thus, if you were writing such a function, it would be very reasonable to write

    double square_root( double number )
    {
        if( number < 0.0 )
            throw domain_error( "square root of a negative argument" );
    
        // Continue with assurance that "number" is zero or positive.
    }
    

    Note that this is a logic error because a correct program should never attempt to feed a function an invalid argument.

  2. class invalid_argument : public logic_error {...};

    This exception class is very similar in concept to the domain_error class above. However, this exception would be a more appropriate choice for a non-mathematical function to use. For example, suppose you are writing a function that accepts a username. Suppose also that on the platform you are using usernames are eight characters or fewer. You might do:

    void process_user( const string &username )
    {
        if( username.length( ) > 8 )
            throw invalid_argument( "long user names can't be processed" );
    
        // Username parameter looks okay.
    }
    
  3. class length_error : public logic_error {...};

    According to the standard: "The class length_error defines the type of objects thrown as exceptions to report an attempt to produce an object whose length exceeds its maximum allowable size." For example, if you had a class that maintained a buffer and your caller attempted to place more material in that buffer than you had space, it would be appropriate to throw a length_error exception as a response.

    You might be tempted to throw a length_error in the case of the extra long username in the previous example. However, the incoming string is not in error by being so long (the string itself is fine). The problem is that such a long string is unacceptable as input to the process_user function. The invalid_argument exception is a much better choice in that case. You can see, however, that deciding exactly which exception type to throw is not always obvious.

  4. class out_of_range : public logic_error {...};

    According to the standard: "The class out_of_range defines the type of objects thrown as exceptions to report an argument value not in its expected range." This sounds very much like a domain error exception except that domain errors are best used only with "pure" mathematical functions. I would suggest using the out of range exception whenever a numerical argument to a non-mathematical function is out of range.

    Personally, it seems to me that this exception class should be derived from invalid_argument since out or range arguments are a special kind of invalid arguments. Wouldn't you want the handler for an invalid_argument to also catch and handle out_of_range? I think so. The C++ standard does not always make perfect sense. However, this example does illustrate that designing an exception hierarchy is not necessarily a trivial exercise. Building a program with high-quality error handling requires thought.

  5. class range_error : public runtime_error {...};

    The range error exception should be used when a purely mathematical function attempts to produce a value that is not in the acceptable range of the function.

  6. class overflow_error : public runtime_error {...};
    class underflow_error : public runtime_error {...};

    Overflow and underflow errors refer to when a numeric calculation produces a result that can't be represented by the finite range of the type used. In a sense, these are types of range errors. However, the failure in this case is not really with the computation but rather with the limited way in which the result of that computation is represented.

Please note that you are free to derive your own exception classes from the standard classes. In some cases, that may make more sense than just using the general purpose classes directly. For example

class ConnectError : public runtime_error {
public:
    ConnectError( const string &m ) : RuntimeError( m ) { }
};

Failure to make a network connection is a type of runtime error. It's not the program's fault if the network doesn't work.

class BadIpError : public invalid_argument {
public:
    BadIpError( const string &m ) : invalid_argument( m ) { }
};

This exception might be used by a network library function to signal that an invalid IP address was given to it as an argument. This is a type of logic error since a correct program shouldn't be giving an invalid argument to one of its functions.

Keep in mind also that you can add additional members and methods to your exception classes if you find that useful. An application that catches your classes specifically would be able to use those methods in the handler. Of course, an application that catches your exception by way of the general base class will know nothing about those extra methods.

If you want to do something like what I show above and you also want your network exceptions to be derived from a generic NetworkError class, you can do so using multiple inheritance. This is one situation where multiple inheritance is useful.

class ConnectError : public std::runtime_error,
                     public NetworkError {
public:
    ConnectError( const std::string &m ) : std::runtime_error( m ) { }
};

Here I'm assuming that NetworkError's constructor has no parameters. If that isn't true, those parameters could be handled in the usual way for multiple inheritance. With this definition, a ConnectError could be caught by either a std::runtime_error handler or by a NetworkError handler.

The bad_alloc exception

The C++ standard defines a few other exception classes that are derived from std::exception but that are not derived from either logic_error or runtime_error. The most important of these exceptions is std::bad_alloc. The library operator new throws bad_alloc if it is unable to allocate the requested memory. Since running out of memory is a common possibility, you will find yourself thinking about bad_alloc exceptions more often and more deeply than probably any other.

To understand the pervasiveness of bad_alloc consider that copying almost any non-trivial object involves allocating memory for the data in the copy. Those allocations could, in general, all throw a bad_alloc. For example:

#include <new>         // Where std::bad_alloc is defined.
#include "Matrix.hpp"  // Some third party matrix class.

void f( )
{
    Matrix x, y, z;

    try {
        // Give y and z values.

        // Might throw std::bad_alloc while the matrix operator* allocates
        // space for the result matrix, while the result is copied to a
        // temporary matrix, or while the temporary is assigned to x.
        //
        x = y * z;
    }
    catch( std::bad_alloc ) {
        std::cerr << "Ran out of memory in f( )" << endl;
    }
}

Of course, this function might ignore exceptions and let the bad_alloc propagate to the caller of f. In either case, however, there are many places where a bad_alloc might be thrown. Notice that in the code above I do not name the exception in the handler. This is perfectly acceptable if you don't intend to use the exception object in the handler's code.

Note: Depending on how the matrix class is implemented, there might be much less copying than meets the eye in my example above. With less copying, there would also come fewer places where a bad_alloc might be thrown. Exactly when an object's data gets copied is often very significant information when it comes to evaluating a function's exception safety. Hopefully, the matrix class fully documents this matter. Alas, often such documentation is lacking.

Exceptions vs assertions

The C library allows programmers to include "executable assertions" in their programs using the assert macro defined in <assert.h>. Here is an example of how the assert mechanism works.

#include <assert.h>

double square_root( double number )
{
    assert(number >= 0.0);

    // If we get here the assertion above must have been true.
}

The assert macro tests the condition provided. If it is true, nothing is done. However, if the condition is false a message is displayed and the program is aborted. Usually the message includes the expression in question as well as the line number and file name of the assertion that failed.

Assertions are supposed to check for "impossible" events. The condition in the assertion should be such that in a correct program it is always true. If the assertion fails, the program will print a cryptic message (it's cryptic to the end user) and abort. Assertions are intended to be used during development to help catch bugs early during testing and to catch them in a way that makes them easier to find. Since the execution time required to do the assertion checking may be a problem in some programs, it is often (but not always) the case that assertion checking is removed from programs before they are deployed.

In fact, if you define the macro NDEBUG before including <assert.h>, the assert macro expands to nothing. This allows you to easily compile a program with assertions and one without. For example

$ gcc -o myprog -g myprog.c
$ gcc -o myprog -DNDEBUG -O myprog.c

The first command will produce an executable with the assertions and debugging information enabled. It would be a suitable version for testing during development. The second command will produce an executable without assertions and with optimization on. It would be a suitable version for final release (be sure to test the release version also... sometimes bugs only show up after optimization).

So far, everything I've said about assertions applies to C and C++ (since C++ inherits the C library). However, you may have noticed that assertions are playing a similar role as the logic error exceptions I mentioned before. The difference is that if a logic error exception is thrown, the program has the option of continuing to run. Furthermore, unless you use some preprocessor tricks of your own, the testing for logic errors is always compiled into your program.

Occasionally, debate breaks out in C++ forums about the relative merits of using assertions or exceptions to handle internal inconsistencies (logic errors) in a program. Many people do not like the fact that assert will abort the program immediately. On the other hand, many logic errors are unrecoverable anyway. Furthermore, the ability to easily remove the assertions from the executable is an attractive feature.

There is no clear right answer to this debate. You will have to form your own opinion as you gain programming experience. If you are writing programs for someone else, that person or organization may have a policy on this matter that you will have to follow. Personally, I rather like assertions and tend toward the policy of using assertions to deal with "impossible" logic errors and exceptions to deal with normal runtime errors. However, I appreciate the other point of view and agree that sometimes using exceptions for logic errors makes sense too.

Summary

  1. It is often desirable to define a hierarchy of classes specifically for exceptions. This allows you to categorize errors into related groups. Application programs can then catch base exceptions by reference as a way of catching exceptions from an entire category. This makes it easy for a library to add new exceptions and for older applications to easily handle those new exceptions.

  2. The C++ standard defines an exception hierarchy based on the class std::exception. The standard defines several derived classes and encourages you to derive classes of your own from the hierarchy it provides. If you do this, it will be easier for application programmers to understand the purpose of your exceptions and to handle them in an appropriate yet generic way.

  3. The exception std::bad_alloc is thrown whenever the library operator new can't find the requested memory. Since many operations involve dynamically allocating memory, the bad_alloc exception is one that must be commonly thought about.

  4. Executable assertions as provided by the C library give you another way of handling logical errors. In C++ you have the option of using C style assertions or throwing C++ exceptions whenever you encounter an "impossible" condition in your program. There is no clear best choice here.

© Copyright 2023 by Peter Chapin.
Last Revised: August 7, 2023