C++ exceptions, templates and life

Mathieu Lacage


    
  

This article is copyrighted to Mathieu Lacage. Copying and/or reuse of this document is not allowed. You may link to this page but you may not redistribute copies and/or modifications of this article.

Revision History
Revision 0.0.420-09-2003

Update build system.

Revision 0.0.322-05-2003
  • Update based on comments by Jonathan Wakely (cow at compsoc dot man dot ac dot uk).

  • Change wording of a paragraph based on comments by Guillaume Laurent (glaurent at telegraph-road dot org).

Revision 0.0.220-05-2003
  • Initial release


Table of Contents

What is an exception ?
Throwing exceptions
Catching exceptions
Exception propagation, exception-safety, smart pointers, templates
Exception myths
Notes
Smart and working exception code
References

A lot of C++ development frameworks do not use exceptions: lots of C++ developers shy away from what they feel is an overly complex feature of the language which they don't understand. Lots of articles available on the internet contribute to this feeling by describing lots and lots of tricky aspects of C++ exceptions. Most of these articles deal solely with the so-called exception safety property of C++ code.

Most such articles usually assume the reader is familiar with the normal aspects of exceptions: their throwing and handling. These articles furthermore seem to hold these aspects as simple enough that they do not deserve much more discussion. However, if you actually try to write exception code, a lot of practical and theorical questions arise, none of which are answered by any of the introductions to C++ exceptions found online. After a lot of time spent browsing the internet for good introductions to the basic concepts of exception handling, I finally gave up and went to the ANSI website and bought an electronic copy of the C++ standard. Because I do not expect most programmers to do this and because I do not think reading the C++ standard should be needed to learn C++, I decided to write down what I learned about exceptions :)

What is an exception ?

Exceptions are usually presented as a way to simplify error handling in user code: user code is supposed to split the error handling code which is usually multiplexed with the main code path from the normal non-error code. The following sample shows code with and without exceptions.

// Without exceptions
typedef enum {
  error_alloc,
  error,
  ok
} ErrorStatus;
class Automatic {};
class Foo 
{
public:
        ErrorStatus DoAction (void) {return ok;}
};
class Bar 
{
public:
        ErrorStatus DoAction (void) {return error;}
};

ErrorStatus f (void)
{
        Automatic a;
        Foo *foo = new Foo ();
        if (foo == 0) {
                // a's destructor invoked automatically upon exit.
  	        return error_alloc;
        }
        ErrorStatus status = foo->DoAction ();
        if (status != ok) {
                delete foo;
                // a's destructor invoked automatically upon exit.
                return status;
        }
        Bar *bar = new Bar ();
        if (bar == 0) {
                delete foo;
                // a's destructor invoked automatically upon exit.
   	        return error_alloc;
        }
        status = bar->DoAction ();
        if (status != ok) {
                delete foo;
                delete bar;
                // a's destructor invoked automatically upon exit.
                return status;
        }
        delete foo;
        delete bar;
        // a's destructor invoked automatically upon exit.
        return ok;
}

int main (int argc, char *argv[])
{
        ErrorStatus status;
        status = f ();
        if (status != ok) {

        }
        return 0;
}

and with exceptions:

class Error {};
class Automatic {};
class Foo
{
public:
	void DoAction() throw(Error) { throw Error(); }
};
class Bar 
{
public:
	void DoAction() throw(Error) { throw Error(); }
};

void f()
{
	Foo* foo = 0;
	try {
		Automatic a;
		foo = new Foo;
		foo->DoAction();
		Bar* bar = 0;
		try {
			bar = new Bar;
			bar->DoAction();
		} catch (...) {
			delete bar;
			throw;
		}
		delete foo;
		delete bar;
		// a's destructor invoked automatically after leaving this scope
	} catch (...) {
		delete foo;
		throw;
	}
}
int main (int argc, char *argv[])
{
        try {
                f ();
	} catch (Error &e) {

        }
}

Both of these code listings try to invoke multiple methods on multiple objects which can all trigger errors. The errors are caught by the Bar method, they are handled localy by freeing the allocated memory and are propagated to the caller in case the caller might want to do something with it.

The syntax for the exception version seems rather straightforward (we'll revisit exception throwing syntax and semantics as well as exception catching and propagation syntax and semantics in the following sections): To return an error, rather than execute the statement return error;, you execute the statement throw Error ();. At this point, the C++ runtime is triggered and unwinds the call stack: it parses the stack from the inner-most function to the outer-most function and invokes the first code block identified by a matching catch statement in one of these functions. The unwinding stops when either the C++ runtime has found a matching catch block which does not rethrow the exception or when it reaches the top of the function stack. At each level, the unwinding routine invokes the destructors of any temporary variables left on the stack.

This process is absolutely similar in terms of functionality to the previous exception-free code which returned from the function as soon as an error was found and propagated the error code to its calling function, hoping that someone higher up in the call hierarchy knows better what to do with this error:

Table 1. Exceptions vs Error codes

 Error codeExceptions
Raise errorreturn error;throw Error ();
Propagate errorif (status != ok) { /* error code*/; return status;}try {} catch (condition) { /* error code */; throw;}
Treat errorif (status != ok) {/* do something about it */;}try {} catch (condition) { /* error code */;}
Declare errorErrorCode foo (void);void foo throw (ErrorCode);
Temporary destructor invocationInvoked whenever the return statement is reachedInvoked during unwinding after any present catch block is executed

Of course, there are a number of rather subtle differences:

  • The exception version of the code can grow pretty deep in terms of indenting which makes it (in my opinion) rather ugly and unreadable. Readers who believe they are smarter than me can try to do what is described in the section called “Smart and working exception code”.

  • Exception declaration in method/function signatures is optional. That is, if a function raises an exception in a code block its declaration does not need to specify that it raises the exception. This property has a rather unpleasant consequence: it means you need to be ready to see _any_ exception thrown from any function/method.

  • The constructor in the exceptions version can throw exceptions (This is the only way to return an error from a constructor but it is better not to throw unless really needed from a constructor) while the error code version cannot return any error code since a constructor does not return any value. Some sick readers might imagine they can give a reference or a pointer to the constructor and have the constructor assign its return value to this parameter. This looks horribly ugly and feels like a just fscking truly scary idea but it is not possible with an object's default constructor which, by definition, does not have any parameters.