Exceptions - The Basics

Exceptions - The Basics

By Steve Cornish

Overload, 7(33):, August 1999


This is the start of a 3-part series of articles describing C++ exceptions. In this part we deal with:-

  • Error handling & exceptions

  • Throwing exceptions

  • Handling Exceptions

  • Exception Remapping

Error handling

Code has two error-related responsibilities; handling errors detected elsewhere (for example input errors from stream libraries), and reporting errors that cannot be dealt with here (for example file access permissions).

Traditional techniques for dealing with errors that cannot be dealt with locally are:

  1. terminate the program

  2. return a value representing an error

  3. return a legal value and pretend nothing happened

  4. call a predefined error function

  5. ignore the error

The first option, to terminate the program, is useless and rude. Usually no one should have to tolerate that kind of behaviour from software. For some standalone systems, though, having the program restarted is the best response to a program failure.

The second option, to return a value representing an error is what most people would think of as an acceptable strategy. However, it appears that the more frequently a function is used (without error), the more complacent programmers get about checking the return value. For example, who checks the return of printf() ? stdout can be redirected to a hard disk file, and hard disks can fill up!

The third option (return a legal value, and pretend nothing happened) is evil on a par with the first option. The calling function has no knowledge that something went wrong, and so cannot take measures to correct it. The error continues to exist in the program's state until either the program falls over in a molten mass, or provides incorrect results seemingly at random. This shouldn't ever get past testing stages.

The fourth option seems the most reasonable choice, but what would a specified error handling function be able to do after logging the error? See the other options…

The last option is similar to the second, but you just don't care about errors. No database handle? Just access the records anyway! Don't expect to see release 2.0.

Due to the increasing popularity of libraries, components and distributed architectures, we now have common situations where the traditional techniques of error handling are all inadequate. This is where errors propagate from "distances" (for example across library boundaries or deep inside code). As an example, Company A writes a library that Company B wants to use. The library reports errors and failures, but there is no way to force Company B's programmers to check the return values for errors. Or at least there wasn't. Exceptions cannot be ignored in the same way that a burning juggernaught cannot be ignored.

So what is an exception?

Exceptions in C++ are alternatives to the traditional methods of dealing with errors.

Exceptions came about because library writers know how to detect errors, but don't know what to do with them, and users know how to deal with them, but don't know how to detect them. This paradox is solved by the way that exceptions separate error reporting and error handling. The exception is used to help deal with error reporting. Error handling is dealt with by the exception-handler.

C++ exceptions were designed to be most useful when an unexpected error occurs at a significant distance from any code that can possibly handle it. (Steve Clamage, Sun Microsystems)

If you are a C programmer, think of exceptions as an OO-friendly longjmp() that takes care of stack unwinding for you.

Throwing exceptions

To report the error, you throw an exception. An exception is usually an object (instance of class / struct) encapsulating appropriate state information for the error. Using built in types for exceptions is possible, but not recommended (how would you discriminate between exception types if both you and a library's author had used int as an exception type?). The type of the object should reflect the type of the error. For example, out_of_bounds.

In the event of an error, the program can construct the exception object and report the error by throwing it. For example:

const int array::integerAt(size_t index) const
{
   if ( index >= (this->size()) )
   {
      throw out_of_bounds("Bad Index");
   }
   // otherwise return requested index
   return ...;
}

If the exception type is a class, it must have an accessible copy-constructor (or default copy-constructor). Otherwise the program is ill-formed (i.e. shouldn't compile).

The throw expression initialises a temporary object of the type specified in the expression (with differences - arrays and functions are changed to pointers, and const-volatile qualifiers ( cv-qualifiers ) are removed). The temporary exists as long as there is a handler being executed for this exception. In the case where the exception is rethrown, the exception is reactivated with the existing temporary object.

Handling an exception

A function can indicate that it is willing to catch exceptions of certain types by declaring a try-catch block. For example:

try
{
    // code which may throw
    // an exception
}

catch ( range_error& )
{
    cerr << "oops!" << endl;
}

The catch block is called the exception handler. It can only be used immediately after a try block, or after another catch attempt. So if any code in a try block (or code called from a try block) throws an exception, the handlers will be examined to see if they are willing to handle the exception.

Throwing an exception transfers control to a handler. An object is created and the type of that object determines which handlers can catch it. For example, consider the following:

try 
{
    throw NoNachos;
}
catch ( OutOfFood& )
{
    // caught here? - 1
}
catch ( NoNachos& )
{
    // or here? - 2
}
catch ( NoCola& )
{
    // or caught here? - 3
}

With no further information, we assume the exception is caught in the second handler. However, if NoNachos is part of an inheritance hierarchy like this:

class OutOfFood {..};
class NoNachos : public OutOfFood {..};
class NoCola : public OutOfFood {..};
class NoIceCream : public OutOfFood {..};

In this case, handler 1 would be the first matching handler for the NoNachos since OutOfFood is an unambiguous base class of NoNachos.

The following table summarises when a handler matches a throw expression with an object of type E.

Handler type Matching exception type
Cv T or cv T& E and T are the same type (ignoring cv-qualifiers)
Cv T or cv T& T is an unambiguous public base class of E
Cv1 T* cv2

E is a pointer type that can be converted
to the handler type by

a standard pointer conversion
a qualification conversion

Catch all

catch(…) specifies a match for any exception. Since exception handlers are tried in order of appearance, this handler ought to be the last handler for its try block to avoid "hiding" the other handlers. The compiler should issue a diagnostic if you have handlers after the catch all.

try 
{
    // stuff
}
catch ( exception1 &e1 )
{
    // handle exception1
}
catch ( exception2 &e2 )
{
    // handle exception2
}
catch ( exception3 &e3 )
{
    // handle exception3
}
catch (...)
{
    // generic handler
}

Stack unwinding

If no handlers are found immediately following the try-block, the stack is unwound to the next enclosing try block, and these handlers are checked for a match. As control passes from the throw-expression to the handler, destructors are called for all automatic objects constructed since the (current) try block was entered. The automatic objects are destroyed in reverse order of their construction. This is known as stack unwinding . This can cause problems if references and pointers are used to acquire resources.

int main()
{
    try 
    {
       f("file.txt", "r");
    }
    catch(...)
    {
       cerr << "Problem with file" 
            << endl;
    }
    return 0;
}

void f(const char *name,
       const char *mode)
{
   FILE *fp = fopen(name, mode);
   char *buf = new (nothrow) char[4096];
         // parse file etc - #
   delete[] buf;
   fclose( fp );
}

Ignore for now the ( nothrow )- this will be explained next month. In the above example, the function f() takes a filename and file open mode, and parses the file. However, if the code in # were to throw, the stack would be unwound to the beginning of the enclosing try-block (in main). This means that fp and buf go out of scope without having their resources released. Hence we have potential resource leaks.

What if we had a class to hold the file as a collection of lines:

class FileInspect
{
    class Line { /* … */ };
    Line *lines[];
    // other details
public:
    FileInspect()
       { lines = new Line[4096]; }
    ~FileInspect() { delete[] lines; }
    // other details
}

and we redefined function f() above as:

void f(const char *name, const char *mode) { FILE *fp = fopen(name, mode); FileInspect ft; // parse file etc - # delete[] buf; fclose( fp ); }

Suppose again, that at some point of parsing the file, an exception occurs at the point #. This time, the FILE* fp is the only resource lost, because FileInspect ft is an atomic variable. Or is this not the case?

The destructor of FileInspect calls delete[] lines which causes the destructor for the Line object to be called 4096 times. It wouldn't be unfeasible for Line::~Line to throw an exception (after all, a line buffer was in use when the first exception was thrown). So which exception takes precedence? The answer is far more horrible than you can imagine: neither.

If a destructor called during stack unwinding throws an exception (without handling it itself), terminate() is called (see the section on special functions next month). Therefore, it is good practise not to throw exceptions in destructors (indeed, no destructor in the C++ library throws). See the section on Exception Specifications next month for a common misconception on preventing exceptions.

So, what happens if no matching handler exists in any surrounding try-block? If an uncaught exception propagates out of main(), the terminate function is called (again, see the section on special functions next month).

Nested try-blocks

Try-catch blocks can be nested. Should you feel dark needs, goto, break, return and continue can be used to transfer control out of a try block. They cannot transfer control into one, however.

try
{
    GetNachos();
}
catch (const NoNachos &nn)
{
    // no nachos - lets get Ben & Jerrys
    // default is "Cherry Garcia" flavour
    try 
    {
        GetIceCream();
    }
    catch (const NoIceCream &nic)
    {
        // deal with no ice-cream
    }
}

Rethrowing

A throw expression with no operand rethrows the current exception being handled. If no exception is being handled, throw calls terminate(). A rethrown exception is no longer considered to be caught. An exception is considered finished when the corresponding catch clause exists (or when unexpected() exists after being entered due to a throw - see special functions for details on unexpected() ). In this situation, the exception is reactivated with the existing temporary.

FILE* fp = fopen("filename.txt", "r");
try 
{
    UseFile(fp);
    fclose(fp);
}

catch (...)
{
    fclose(fp);  // close file
    throw;       // rethrow original
                 // exception
}

Here an exception has occurred. We don't want to handle it here, but we can't leak the file resource. So we catch the exception, close the file, and rethrow it to be handled elsewhere.

Exception remapping

In a layered architecture, each layer may have its own exceptions, and each exception will (probably) only have context within that layer. For example, an exception which represents a low-level parity error on byte 2 packet 15 is probably not much use at the user interface layer, unless it has been washed, shaved, dressed and presented to the users as a "Data transfer error". Even though the error has not been dealt with, it has been changed to a different guise to make it acceptable to a different layer.

Consider the situation where you are using a third party library with their own exception classes.

/**** third party code ****/
void Obscure() 
      throw (UglyDucklingException);
/**** your code ****/
void UsefulFunction()
      throw (BeautifulSwanException)
{
  try
  {
     ..
     Obscure()
  }
  catch (UglyDucklingException &ude)
  {
     throw BeautifulSwanException(ude);
  }
}
/**** client code ****/
try 
{
    UsefulFunction();
}
catch (const BeautifulSwanException &e)
{
    // everyone's happy
}

The function UsefulFunction() attempts to use the third party function Obscure() which may throw an exception you don't want exposed to clients who use your software. The call to Obscure() is in a try block which catches the ugly UglyDucklingException exception. The handler then throws an instance of BeautifulSwanException, which you are happy to let clients deal with. The UglyDucklingException is considered handled, and the only exception to propagate from the function is the BeautifulSwanException.






Your Privacy

By clicking "Accept Non-Essential Cookies" you agree ACCU can store non-essential cookies on your device and disclose information in accordance with our Privacy Policy and Cookie Policy.

Current Setting: Non-Essential Cookies REJECTED


By clicking "Include Third Party Content" you agree ACCU can forward your IP address to third-party sites (such as YouTube) to enhance the information presented on this site, and that third-party sites may store cookies on your device.

Current Setting: Third Party Content EXCLUDED



Settings can be changed at any time from the Cookie Policy page.