Exceptions

The intent of this article is to provide a quick but complete introduction to C++ exceptions. The exception handling mechanism, common usage schemes and performance issues of exceptions versus return codes will all be covered.

So why are exceptions a good thing?

  • Exceptions separate error handling code from normal code. This is a good thing, because it makes the code much more readable. Compare a long series of if conditions, partly for error checking, partly for branching, nested with function calls versus simply writing down the function calls one after another.
  • Exceptions cannot be ignored like return values. Although programmers new to exceptions are often confused by this fact, it is far better to notice an error than letting your application continue to run into an undefined state, often causing follow-up errors which make it much harder to find the real cause of a problem.
  • Exceptions can carry rich error information, specifying the exact cause of an error and which operations were carried out at that moment. To provide similar features using error codes, you would be required return entire error structures from each function called, or to allocate/deallocate additional error information on the heap.
  • It is possible to derive exception classes from each other. This way, you can categorize you errors better, while still making it easy for the user to catch an entire kind of error types (like catching all file access errors, including errors because of locked files, missing files and corrupted files).
  • Using exceptions, the return values of your functions can be used for other purposes again. This is important because it enables your functions to be used in initialization lists of class constructors or as arguments to other functions, without the need for temporary variables.

The Problem

The average programmer commonly makes use of return codes to communicate the result of a function call back to the caller. There’s nothing wrong with it, but it’s just as common that a large portion of these return codes are not checked at all, sometimes because the programmer thought it was unlikely that a specific function might fail just there, sometimes also out of pure lazyness.

If an error goes by undetected, the program will be in an undefined state, doing strange things or crashing in a place where it is next to impossible to locate the original error which has caused the instability. To write robust code, all errors have to be detected and properly handled (or returned to the caller if they cannot be handled in the current scope). But doing so requires a lot of grunt work, littering each and every function with possibly dozens of if()s.

Think of exceptions as an automation of this process. When an exception is thrown, the current function automatically returns to its caller, which has the option to either catch the exception, or to ignore it, causing the exception to travel further upward in the call stack. Once the exception leaves the main() procedure without being caught, the program is aborted.

Introduction

Exceptions aren’t hard to master. Let’s start with an example:

#include <iostream>

void foo() {
  throw 123;
}

int main() {
  foo();

  std::cout << "here" << std::endl;
}

What will happen here ?

foo() will be called from within main(). Then, an exception will be thrown by foo(), travel upwards to the caller and further, probably causing some kind of message from your compiler’s run time library ;)

Notice that the line of code, directly after the call to foo(), will not be executed because the exception smashes back into the main() function. Next we’ll try to catch the exception, thus preventing it from aborting our program:

#include <iostream>

void foo() {
  throw 123
}

int main() {
  try {
    foo();
  }
  catch(int number) {
    std::cout << "exception caught: " << number << std::endl;
  }

  std::cout << "here" << std::endl;
}

This time, the program won’t crash. The exception we’ve thrown is going to be caught within main(), display the message contained in the catch block and then normally continue execution after the catch block. Try commenting out the throw instruction and look what changes when the program is run.

Of course, a function containing an exception handler can call another function, which also contains an exception handler. Actually, you can even nest exception handlers directly:

#include <iostream>

int main() {
  try {

    try {
      throw 123;
      std::cout << "leaving inner try block normally" << std::endl;
    }
    catch(int) {
      std::cout << "exception caught from inner try block" << std::endl;
      throw;
    }

    std::cout << "leaving outer try block normally" << std::endl;
  }
  catch(int) {
    std::cout << "exception caught in outer try block" << std::endl;
  }

  std::cout << "program ends" << std::endl;
}

You can see two new things here: A nested try block which will catch the exception that is thrown before it reaches the outer try block, and another way to use the throw instruction that allows us to re-throw exceptions inside a catch block: If you write throw; without any arguments in an exception handler, the exception that was caught will be re-thrown.

Try commenting out this line and see what happens!

Exception classes

The standard C++ library provides us with a set of exception classes, which is used by the C++ library functions and which we can use in our own applications. These are made available to your code by including the stdexcept header.

Some of the exception classes provided by the C++ standard library are:

  • std::exception – Root exception class. All exception classes in the standard library are directly or indirectly derived from this class.

    • std::runtime_error – Base class for all errors which are only detectable at runtime, like missing files, out-of-memory situations and the like.

    • std::logic_error – Base class for all errors which are caused by a programming error. Reasons for these errors can usually be permanently resolved. Typical cases are invalid array indices, uninitialized objects and more.

      • std::invalid_argument – Derived from logic_error, this exception is thrown when an invalid argument is encountered. Would be the right exception to throw if your hex2int() function is called with a string containing the letters “0xabcw” ;)

      • std::out_of_range – Derived from std::logic_error, should be thrown if an index passed as an argument to a function or method is not within the valid range (eg. exceeds array boundaries).

There are more exception classes in the standard library which you can look up in your compiler’s documentation.

There’s nothing preventing you from creating your own exception classes, even the seperation of runtime_errors and logic_errors may not be all that useful to you. But let’s stick with the standard exception classes for now, so I’ll be able to quickly demonstrate the benefits of exception classes.

#include <iostream>
#include <cmath>

void foo() {
  if(std::rand() % 2)
    throw std::invalid_argument("I am an invalid_argument error");
  else
    throw std::out_of_range("I am an out_of_range error");
}

int main() {
  try {
    foo();
  }
  
  catch(const std::logic_error &error) {
    std::cout << "exception caught: " << error.what() << std::endl;
  }

  std::cout << "program ends" << std::endl;
}

Both exceptions (invalid_argument and out_of_range) will be caught in main() because both are derived from the class by which we catch the exception, logic_error. You can catch an entire category of exceptions, provided the category shares a common base class by which its exceptions are classified. Notice that if you re-throw an exception that is caught as one of its base classes, it will not become an actual instance of the base class.

When using exception classes, always throw by value. Throwing an exception allocated with new will work, too, but there’s no place where the exception could be safely deleted again. As shown in the previous example, exceptions thrown by value can still be caught by reference, thus not preventing full usage of polymorphism.

Should you ever need to catch all exceptions, regardless of their type, there’s a special argument for catch which allows you to do so:

#include <iostream>

int main() {
  try {
    new int[-1];
  }
  catch(...) {
    std::cout << "exception caught" << std::endl;
  }
}

In the above example, new throws an exception because the amount of memory we’re requesting is invalid. We can catch this exception without knowing its type.

Using exceptions

Now that we know the mechanics of C++ exceptions, let’s see how and where we can use them. An exception is thrown, as is indicated by its name, in exceptional situations. Exceptions should not be used to control normal program flow.

However, some programmers understand an exceptional situation as a situation in which the application can not continue to run and has to be aborted. If that was what exceptions were intended for, they would be nothing more than a fancy, but redundant implementation of the C runtime’s abort() function.

An exceptional situation is exceptional when the current scope cannot recover from it. For example, a function that is called to load a bitmap, the file name of which being provided as an argument, will reach an exceptional situation when the file does not exist. It can not resolve this error on its own, neither does it know whether this bitmap is unimportant and can be silently replaced by a white square.

Image *loadImage(const string &sFilename) {
  ifstream ImageFile(sFilename);

  if(!ImageFile)
    throw FileAccessException(
      string("The file '") + sFilename + "' could not be opened"
    );

  // ...continue to load the image...
}

For the caller, this situation may not be that exceptional anymore. Maybe the user just selected an invalid file in the file selector and expects all but the entire program aborting, trashing all his previous work. From the programmer’s side, this means that when the image is loaded, after being selected by the user, all exceptions relating to file accesses can be handled by displaying a little message box to the user and continuing normal program flow.

void onFileOpen() {
  string sFilename = showFileSelector();

  try {
    Image *pNewImage = loadImage(sFilename);
    createEditorWindow(auto_ptr<Image>(pNewImage));
  }
  catch(const FileAccessException &error) {
    showMessageBox(string("Error loading image: ") + error.what());
  }
}

Throw exceptions when an error (an exceptional situation) occurs which can not be handled in the current scope. This is the most important and also the only rule to remember.

Drawbacks

  • A place where you would not want to use exceptions is within tight loops that have to run as fast as possible. Even if no exception is thrown, an exception handler can slow down the speed of function calls and object construction a bit. This is because of the stack unwinding code which some compilers will generate. I know there are some programmers which see their entire application as one tight loop which has to run as fast as possible on every single line of code. I can’t help those ;)

  • When refactoring existing code to use exceptions, you have to be very careful not to create situations like this one:

    void foo() {
      int *pInts = new int[123];
    
      int theNumber = senseOfLife(pInts);
      delete []pInts;
    
      if(!theNumber)
        reportError("Unable to retrieve sense of life");
    }

    If the senseOfLife() function would be modified to throw an exception when it fails to find the sense of life, the previous snippet of code would still work, but the exception would prevent the cleanup code from being run. Apart from memory leaks, you can generate all kinds of awkward behavior from this exact type of code – try throwing an exception through WindowProc() and catch it in your message pump for a taste :)

This does not mean that there’s an inherent problem with exceptions, just that you should be careful when equipping projects with exception handling that have been built by an exception-unaware programmer using return codes originally.

Leave a Reply

Your email address will not be published. Required fields are marked *

Please copy the string 3GUZck to the field below:

This site uses Akismet to reduce spam. Learn how your comment data is processed.