Contents | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Glossary | Examples

Lecture 10: Exceptions

10.1 goto

C++ exceptions are like C++/C long jumps which are in turn like global goto statements. The goto remember is the sole application of "function" scope in C++/C and allows the transfer of control between any two places in a function.

f() {
  ...
  label:
  ...
  goto label;
}

Very flexible, but it certainly can be abused. For example, transferring control from outside of a loop to inside it will almost certainly cause problems. Two valid uses are:

  1. Break out of nested loops, for example:
    for (..;..;..)
      for (..;..;..)
        for (..;..;..)
          for (..;..;..)
          {
            ...
            if (get_out) goto xit;
            ...
          }
    xit:
    ...
    

    This could be accomplished by defining flags and checking the flags in the control statements of the for loops, but that solution would slow things down and make the code more intricate.

  2. Exception handling, for example:
    f()
    {
      ...
      if (problem) { error handling setup code; goto handle_error; }
      ...
      if (problem) { error handling setup code; goto handle_error; }
      ...
      return 0; // normal return
      handle_error: // error handling code
      ...
      return error_code;
    }
    

This allows sharing of error handling code, and doesn't complicate the flow of control too much.

Other than these applications, the traditional advice of avoiding the use of goto is sound in most cases.

10.2 longjmp()

The ANSI C long jump is accessed via the include statement

#include <setjmp.h>

which declares a type (jmp_buf) and two functions:

int setjmp(jmp_buf);
void longjmp(jmp_buf, int);

The jmp_buf type is used to store information about the current state of the run-time stack so it can be restored to that state if a long jump is executed. setjmp() initializes the buffer and returns 0. If a long jump occurs, control returns to the original setjmp() call whose return value now becomes that specified by the longjmp() call. It is a global form of goto and should be used in the same manner:

  1. Break out of nested function calls, for example:
    f(TNode *tree)
    {
      TNode *node;
      jmp_buf buf;
    
      switch(setjmp(buf))
      {
      case 0: // initiate search
        search(tree, &node, buf, "hello, world");
        break;
      case 1: // failed search
        ...
        break;
      case 2: // successful search
        ...
        break;
      }
    }
    
    void search(TNode *branch, TNode **found, jmp_buf buf, char *item)
    {
      if (!branch) longjmp(buf, 1);
      else if (strcmp(branch->value, item) < 0)
        search(branch->left, found, buf, item);
      else if (strcmp(branch->value, item) > 0)
        search(branch->right, found, buf, item);
      else { *found = branch; longjmp(buf, 2); }
    }
    

    Again, a system of flags would be another way to handle this, but it would be slower and complicate the code.

  2. Exception handling, for example:
    main()
    {
      jmp_buf buf;
      switch(setjmp(buf))
      {
      case 0:
      main_loop:
        switch(request_choice(menu))
        {
          ...
        }
      case MEM_EXCEPTION:
        // handle non-fatal memory exception
        ...
        // re-enter loop
        goto main_loop;
      case DEVICE_EXCEPTION:
        // handle fatal device exception
        ...
        return code;
      }
      return 0;
    }
    

Like the goto, it must be used in a very structured way, or the code will become unreadable and unmaintainable.

10.3 C++ Exceptions

In C++, exceptions are essentially a more sophisticated and flexible implementation of the long jump idea. Doing the jump is beset with more complexities for the C++ compiler implementor, since all the objects which go out of scope during the unwinding process for the run-time stack must be destroyed. However, this is of course a boon for the programmer. A long jump in the C++ context will not result in this happening.

Syntax: try block followed immediately by one or more catch blocks. try block takes place of setjmp(). Usage possibilities are approximately as with the long jump:

  1. Break out of nested function calls, for example:
    f(TNode *tree)
    {
      TNode *node;
    
      try {
        search(tree, &node, "hello, world");
      }
      catch(int return_code)
      {
        switch(return_code)
        {
        case 1: // failed search
          ...
          break;
        case 2: // successful search
          ...
          break;
        }
      }
    }
    
    void search(TNode *branch, TNode **found, char *item) throw(int)
    {
      if (!branch) throw 1;
      else if (strcmp(branch->value, item) < 0)
        search(branch->left, found, item);
      else if (strcmp(branch->value, item) > 0)
        search(branch->right, found, item);
      else { *found = branch; throw 2; }
    }
    
  2. Exception handling, for example:
    main()
    {
      main_loop:
      try {
        switch(request_choice(menu))
        {
          ...
        }
      }
      catch(int exception)
      {
        switch(exception)
        {
        case MEM_EXCEPTION:
          // handle non-fatal memory exception
          ...
          // re-enter loop
          goto main_loop;
        case DEVICE_EXCEPTION:
          // handle fatal device exception
          ...
          return code;
        }
      }
      return 0;
    }
    

The above examples mimic the long jump by throwing ints; however, any type can be thrown, and it can be caught by value or by reference. The catch statement is a bit like a one-argument function call, except only polymorphic conversions are available. Thus the first example could be rewritten as:

class FoundNode {
  TNode *m_node;
public:
  FoundNode(TNode *node) : m_node(node) { }
  TNode *node() { return m_node; }
};

f(TNode *tree)
{
  try {
    search(tree, "hello, world");
  }
  catch(FoundNode node)
  {
    if (node.node()) // successful search
    {
      ...
    }
    else // failed search
    {
      ...
    }
  }
}

void search(TNode *branch, char *item) throw(FoundNode)
{
  if (!branch) throw FoundNode(0);
  else if (strcmp(branch->value, item) < 0)
    search(branch->left, item);
  else if (strcmp(branch->value, item) > 0)
    search(branch->right, item);
  else throw FoundNode(branch);
}

When an exception is detected and an object is thrown, the matching catch block (or it can match a base class of object being caught) with the most closely nested try block handles the exception. When there are two or more matching catches for the closest try, the first catch after the try block is used.

If the catch handler wishes to look at the object, it has to give it a name in the header of the catch block.

catch(...) catches anything.

A catch block can "re-throw" exception to more outer catch blocks by just saying throw without specifying an object. Could the re-throw possibly land in a later catch for the same try block?

If no catch block is found for the exception, the function terminate() is called. The function

PFV set_terminate(PFV)

(typedef void (*PFV)() applies here) can be used to install a new function for terminate() to call. By default it calls abort(). Any function installed by set_terminate() should not return.

Functions can declare the exceptions they throw, or that functions they use throw and that they don't catch. The list of declared exception types appears in the throw() declaration after the function signatures above. If throw() appears without any types in the parentheses, that means the function claims that it doesn't throw any exceptions and that it catches any exceptions thrown by functions it calls. The throw() declaration is optional, and exceptions don't need to be declared for a function even if it throws some.

If a function throws an exception type which isn't in its list of declared exceptions, the function unexpected() is called. PFV set_unexpected(PFV) can be used to tailor this. By default unexpected() calls terminate(). If a function doesn't declare exceptions, any exception thrown inside it will be handled outside it somewhere without unexpected() being triggered unless a function outside doesn't handle the exception and has a list in which the exception type doesn't appear.

10.4 C++ Exceptions: Coding Precautions

There are three types of code that exceptions can create problems for (The following notes draw much from Margaret Ellis and Martin Carroll, "Tradeoffs of Exceptions", C++ Report, Vol. 7, No. 3 (March-April 1995), pp. 12-16.):

  1. Doing something (e.g. heap memory acquisition, handler function installations, changing stream's or other object's state) which is to be undone by later code. Problems arise when exception occurs after something has been done, but before it is undone.

    This problem is fixed by making sure all these operations are carried out within the context of construction and destruction of an object. Some "weightless" code can be added to achieve this. For example, instead of coding a function as follows:
    void f()
    {
      PFV *old_new_handler = set_new_handler(my_new_handler);
    
      ... // this code may generate an exception
    
      set_new_handler(old_new_handler);
    }
    
    It would be preferable, when the possibility exists that an exception might be thrown during the course of the function's execution, to code it as follows:
    class install_new_handler {
      PFV *m_old_new_handler;
    public:
      install_new_handler(PVF *new_handler) {
        m_old_new_handler(set_new_handler(new_handler));
      }
      ~install_new_handler() {
        set_new_handler(m_old_new_handler)
      }
    };
    
    void f()
    {
      install_new_handler(my_new_handler);
    
      ... // this code may generate an exception
    }
    
    Even if exceptions are not a problem, this technique has a lot to recommend it since it makes sure whatever needs undoing gets undone without the programmer having to remember to write the appropriate code. In the case of heap memory acquisition, the auto_ptr template from the STL is tailor made for this situation.

  2. Exception gets thrown while inside a constructor. C++ exception handlers don't call the destructor for this object, so object must make sure any partial allocations get undone.

    This can be done using a try, catch and re-throw combination where a universal catch block in the constructor can take care of undoing that which must be undone. For example:
    stack::stack(int sz)
    {
      try {
        val = new char[size = sz];
        notify(); // possibly generates exception
      }
      catch(...)
      {
        delete[] val;
        throw;
      }
      top = val;
    }
    
  3. Exception gets thrown while inside a member function for a class while the object is in an invalid state.

    Same solution as in 2 works here.

Exception techniques for templates. Example stack underflow or overflow. In the case of overflow, the exception report could contain the value of the object being pushed. Exception classes can be templated and inherit from common base class. This allows application to handle stack exceptions in a general or specific way.

10.5 Standard C++ Exceptions

The following exceptions are generated by the C++ language:

bad_alloc, bad_cast, bad_typeid, bad_exception.

The following exceptions are generated by the standard C++ library:

out_of_range, invalid_argument, overflow_error, ios_base::failure.

All these derive from the exception class.

Contents | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Glossary | Examples