ANSI/ISO C++ Professional Programmer's Handbook

Contents


6

Exception Handling

by Danny Kalev

Introduction

Large software applications are built in layers. At the lowest level, you usually find library routines, API functions, and proprietary infrastructure functions. At the highest level, there are user interface components that enable a user to, for instance, fill out a data sheet in a spreadsheet application. Consider an ordinary flight-booking application: its topmost layer consists of GUI components that display contents on the user's screen. These high-level components interact with data access objects, which in turn encapsulate database API routines. At a lower level, the database API routines interact with the database engine. The database engine itself invokes system services that deal with low-level hardware resources such as physical memory, file system, and security modules. In general, severe runtime errors are detected in these lower code layers, which cannot -- or should not -- attempt to handle these errors on their own. The handling of severe runtime errors is the responsibility of higher-level components. In order to handle an error, however, higher-level components have to be informed that an error has occurred. Essentially, error handling consists of detecting an error and notifying the software components that are in charge. These components in turn handle the error and attempt to recover from it.

Traditional Error Handling Methods

In its earlier stages, C++ did not have a built-in facility for handling runtime errors. Instead, the traditional C methods were used for that purpose. These methods can be grouped into three design policies:

Each of these methods has significant drawbacks and limitations in an object-oriented environment. Some of them might be totally unacceptable, particularly in large-scale applications. The following sections examine each of these methods more closely in order to assess their inherent limitations and hazards.

Returning an Error Code

To some extent, this method can be useful in small programs in which an agreed-upon, closed set of error codes exists, and in which a rigorous policy of reporting errors and checking the returned status of a function is applied. However, this method has some noticeable limitations; for example, neither the error types nor their enumerated values are standardized. Thus, in one library the implementer might choose a return value of 0 (meaning false, perhaps) to indicate an error, whereas another vendor might choose 0 to indicate success and any nonzero value to indicate an error condition. Usually, the return codes are shared in a common header file in the form of symbolic constants so that some commonality can be maintained throughout an application or a development team. These codes are not standardized, however.

Needless to say, the process of combining noncompatible software libraries from different vendors or programmers becomes very difficult and confusing when conflicting error codes are used. Another disadvantage is that every returned code has to be looked up and interpreted -- a tedious and costly operation. This policy requires that the return value of every function be checked every time by every caller; failing to do so might lead to runtime disasters. When an error code is detected, a return statement disrupts the normal execution flow and passes the error code on to the caller. The additional code that wraps every function call (to examine the return status and decide whether to continue normally) can easily double the size of the program and cause serious difficulties in the software's maintenance and readability. Worse yet, returning an error value is sometimes impossible. For instance, constructors do not return values, so they cannot use this method to report the failed construction of an object.

Turning on a Global Flag

An alternative approach for reporting runtime errors is to use global flags, which indicate whether the last operation ended successfully. Unlike the return code policy, this method is standardized. The C <errno.h> header file defines a mechanism for examining and assigning the value of a global integer flag, errno. Note that the inherent drawbacks of this policy are not negligible. In a multithreaded environment, the error code that is assigned to errno by one thread can be inadvertently overwritten by another thread before the caller has had a chance to examine it. In addition, the use of an error code instead of a more readable message is disadvantageous because the codes might not be compatible among different environments. Finally, this method requires a well-disciplined programming style that relies on constant checking of the current value of errno.

The global flag policy is similar to the function return value policy: Both provide a mechanism for reporting an error, but neither guarantees that the error is actually handled. For example, a function that fails to open a file can indicate a failure by assigning an appropriate value to errno. However, it cannot prevent another function from attempting to write into the file or close it. Furthermore, if errno indicates an error and the programmer detects and handles it as is expected, errno still has to be reset explicitly. A programmer might forget to do so, thereby causing other functions, which assume that the error has not been handled, to attempt to rectify the problem -- with unpredictable results.

Terminating the Program

The most drastic method of handling a runtime error is simply to terminate the program immediately when a severe error has been detected. This solution averts some of the drawbacks of the previous two methods; for example, there is no need for repetitive examination of the status that is returned from every function call, nor does the programmer have to assign a global flag, test its value, and clear it in a repetitive and error-prone manner. The standard C library has two functions that terminate a program: exit() and abort(). exit() can be called to indicate successful termination of a program (as the final statement in main()), or it can be called in the case of a runtime error. Before returning control to the environment, exit() first flushes open streams and closes open files. abort(), on the other hand, indicates abnormal program termination. It terminates immediately, without flushing streams or closing open files.

Critical applications cannot just halt abruptly on every occurrence of a runtime error. It would be disastrous if a life support machine stopped functioning just because its controller detected a division by zero; likewise, the embedded computer that controls the automatic functions of a manned space shuttle should not halt just because it temporarily loses communication with ground control. Similarly, applications such as the billing system of a telephone company or a banking application cannot break down altogether whenever a runtime exception occurs. Robust, real world applications can -- and must -- do better than that.

Program termination is problematic even for applications, such as an operating system, that are expected to abort in the case of serious runtime errors. A function that detects the error usually does not have the necessary information to estimate the severity of the error. A memory allocation function, for example, cannot tell whether an allocation request has failed because the user is currently using a debugger, a Web browser, a spreadsheet, and a word processor all at once, or because the system has become unstable due to a severe hardware fault. In the first scenario, the system can simply display a message, requesting that the user close unnecessary applications. In the second scenario, a more drastic measure might be required. Under this policy, however, the allocation function simply aborts the program (the operating system kernel, in this case), regardless of the severity of the error. This is hardly applicable in nontrivial applications. Good system design has to ensure that runtime errors are detected and reported, but it also has to ensure a minimal level of fault tolerance.

Terminating the program might be acceptable under extreme conditions or during debugging phases. However, abort() and exit() are never to be used in an object-oriented environment, even during debugging, because they are unaware of the C++ object model.

exit() and abort() Do Not Destroy Objects

An object can hold resources that are acquired by the constructor or a member function: memory allocated on the free store, file handles, communication ports, database transaction locks, I/O devices, and so on. These resources have to be properly released by the object that uses them when it's done. Usually, these resources are released by the destructor. This design idiom is called resource initialization is acquisition (this is discussed in greater detail in Chapter 5, "Object-Oriented Program and Design"). Local objects that are created on the stack are destroyed automatically when their block or function exits. Neither abort() nor exit(), however, invokes the destructors of local objects. Therefore, an abrupt program termination caused by calling these functions can cause irreversible damage: A database can be corrupted, files can be lost, and valuable data can evaporate. For this reason, do not use either abort() or exit() in an object-oriented environment.

Enter Exception Handling

As you have observed, none of the traditional error handling methods of C are adequate for C++; one of the goals of C++ was to enable better and safer large-scale software development than is offered by C.

The designers of C++ were aware of the considerable difficulties resulting from the lack of a proper error handling mechanism. They sought a solution that was free from all the ailments of C's traditional error handling. The suggested mechanism was based on the automatic transfer of control to the system when an exception is triggered. The mechanism had to be simple, and it had to free the programmer from the drudgery of constantly examining a global flag or the returned value of a function. Additionally, it had to ensure that the code sections that handle the exception are automatically informed when an exception occurs. Finally, it had to ensure that when an exception is not handled locally, local objects are properly destroyed and their resources are released before the exception is propagated to a higher handler.

In 1989, after several years of research and a plethora of draft proposals, exception handling was added to C++. C++ is not the first language to offer structured runtime error handling support. Back in the 1960s, PL/1 offered a built-in exception handling mechanism; Ada provided its own version of exception handling in the early 1980s, as did several other languages. But none of these exception handling models fit the C++ object model and program structure. Therefore, the proposed exception handling for C++ was unique, and it has served as a model for newer languages that have appeared since.

Implementing an exception handling mechanism turned out to be a real challenge. The first C++ compiler, cfront, ran on UNIX. Like many UNIX compilers, it was a translator that first transformed C++ code into C, and then compiled the resultant C code. Release 4.0 of cfront was supposed to include exception handling. However, the implementation of an exception handling mechanism that met all the requirements got so complicated that the development team of cfront 4.0 decided to abandon the project entirely after spending a whole year designing it. cfront 4.0 was never released; however, exception handling became an integral part of Standard C++. Other compilers that started to appear later supported it. The following section explains why it was it so difficult to implement exception handling under cfront, and under any other compiler in general.

The Challenges of Implementation of Exception Handling

The difficulties in implementing exception handling arise from several factors. First, the implementation must ensure that the proper handler for a specific exception is found.

Secondly, exception objects can be polymorphic; in that case, the implementation also considers handlers of base classes when it cannot locate a matching handler for a derived object.. This requirement implies a sort of runtime type checking to recover the dynamic type of the exception object. Yet C++ did not have any runtime type checking facilities whatsoever before exception handling was developed; these facilities had to be created from scratch for that purpose.

As an additional complication, the implementation must invoke the destructors of all local objects that were constructed on the path from a try block to a throw expression before control passes to the appropriate handler. This process is called stack unwinding (the stack unwinding process is discussed in further detail later in this chapter). Because early C++ compilers translated the C++ source file into pure C and only then compiled the code into machine code, the implementers of exception handling had to implement runtime type identification and stack unwinding in C. Fortunately, these obstacles have all been overcome.

Applying Exception Handling

Exception handling is a flexible and sophisticated tool. It overcomes the drawbacks of C's traditional error handling methods and it can be used to handle a variety of runtime errors. Still, exception handling, like other language features, can easily be misused. To use this feature effectively, it is important to understand how the underlying runtime machinery works and what the associated performance penalties are. The following sections delve into exception handling internals and demonstrate how to use this tool to create robust, bulletproof applications.


CAUTION: Some of the code samples in the following sections use new exception handling features such as function try blocks and exception specifications. Several compilers do not support these features yet; therefore, it is recommended that you read the technical documentation of your compiler to check whether it fully supports exception handling.

Exception Handling Constituents

Exception handling is a mechanism for transferring control from a point in a program where an exception occurs to a matching handler. Exceptions are variables of built-in data types or class objects. The exception handling mechanism consists of four components: a try block, a sequence of one or more handlers associated with a try block, a throw expression, and the exception itself. The try block contains code that might throw an exception. For example

try 
{
 int * p = new int[1000000]; //may throw std::bad_alloc
}

A try block is followed by a sequence of one or more catch statements, or handlers, each of which handles a different type of exception. For example

try
{
 int * p = new int[1000000]; //may throw std::bad_alloc
 //...
}
catch(std::bad_alloc& ) 
{
}
catch (std::bad_cast&)
{
}

A handler is invoked only by a throw expression that is executed in the handler's try block or in functions that are called from the handler's try block. A throw expression consists of the keyword throw and an assignment expression. For example

try
{
 throw 5; // 5 is assigned to n in the following catch statement
}
catch(int n) 
{
}

A throw expression is similar to a return statement. An empty throw is a throw statement without an operand. For example

throw; 

An empty throw inside a handler indicates a rethrow, which is discussed momentarily. Otherwise, if no exception is presently being handled, executing an empty throw calls terminate().

Stack Unwinding

When an exception is thrown, the runtime mechanism first searches for an appropriate handler in the current scope. If such a handler does not exist,

the current scope is exited and the block that is higher in the calling chain is entered into scope. This process is iterative: It continues until an appropriate handler has been found. An exception is considered to be handled upon its entry to a handler. At this point, the stack has been unwound and all the local objects that were constructed on the path from a try block to a throw expression have been destroyed. In the absence of an appropriate handler, the program terminates. Note, however, that C++ ensures proper destruction of local objects only when the thrown exception is handled. Whether an uncaught exception causes the destruction of local objects during stack unwinding is implementation-dependent. To ensure that destructors of local objects are invoked in the case of an uncaught exception, you can add a catch all statement in main(). For example

int main()
{
  try
  {
    //...
  }
  catch(std::exception& stdexc)   // handle expected exceptions
  {
    //...
  }
  catch(...)   // ensure proper cleanup in the case of an uncaught exception
  {
  }
  return 0;
}

The stack unwinding process is very similar to a sequence of return statements, each returning the same object to its caller.

Passing Exception Objects to a Handler

An exception can be passed by value or by reference to its handler. The memory for the exception that is being thrown is allocated in an unspecified way (but it is not allocated on the free store). Some implementations use a dedicated exception stack, on which exception objects are created. When an exception is passed by reference, the handler receives a reference to the exception object that is constructed on the exception stack. Passing an exception by reference ensures its polymorphic behavior. Exceptions that are passed by value are constructed on the stack frame of the caller. For example

#include  <cstdio>
class ExBase {/*...*/};
class FileEx: public ExBase {/*...*/};
void Write(FILE *pf)
{
  if (pf == NULL) throw FileEx();
  //... process pf normally
}
int main ()
{
  try
  {
    Write(NULL); //will cause a FileEx exception to be thrown
  }
  catch(ExBase& exception) //catch ExBase or any object derived from it
  {
  //diagnostics and remedies   }
}

Repeatedly copying objects that are passed by value is costly because the exception object can be constructed and destroyed several times before a matching handler has been found. However, it occurs only when an exception is thrown, which only happens in abnormal and -- hopefully -- rare situations. Under these circumstances, performance considerations are secondary (exception handling performance is discussed at the end of this chapter) to maintaining an application's integrity.

Exception Type Match

The type of an exception determines which handler can catch it. The matching rules for exceptions are more restrictive than are the matching rules for function overloading. Consider the following example:

try
{
  throw int();
}
catch (unsigned int) //will not catch the exception from the previous try-block
{		 
}

The thrown exception is of type int, whereas the handler expects an unsigned int. The exception handling mechanism does not consider these to be matching types; as a result, the thrown exception is not caught. The matching rules for exceptions allow only a limited set of conversions: For an exception E and a handler taking T or T&, the match is valid under one of the following conditions:

If E and T are pointers, the match is valid if E and T are of the same type or if E points to an object that is publicly and unambiguously derived from the class that is pointed to by T. In addition, a handler of type array of T or function returning T is transformed into pointer to T or pointer to function returning T, respectively.

Exceptions as Objects

As you have probably noticed, the traditional convention of returning an integer as an error flag is problematic and unsatisfactory in OOP. The C++ exception handling mechanism offers more flexibility, safety, and robustness. An exception can be a fundamental type such as int or a char *. It can be a full-fledged object as well, with data members and member functions. Such an object can provide the exception handler with more options for recovery. A clever exception object, for example, can have a member function that returns a detailed verbal description of the error, instead of letting the handler to look it up in a table or a file. It can have member functions that enable the program to recover from the runtime error after the error has been handled properly. Consider a logger class that appends new records to an existing log file: If it fails to open the log file, it throws an exception. When it is caught by the matching handler, the exception object can have a member function, which creates a dialog box. The operator can choose recovery measures from the dialog box, including creation of a new log file, redirecting the logger to an alternative file, or simply allowing the system to run without a logger.

Exception Specification

A function that might throw an exception can warn its users by specifying a list of the exceptions that it can throw. Exception specifications are particularly useful when users of a function can view its prototype but cannot access its source file. Following is an example of specifying an exception:

class Zerodivide{/*..*/};
int divide (int, int) throw(Zerodivide);   // function may throw an exception
                                           // of type Zerodivide, but no other

If your function never throws any exceptions, it can be declared as follows:

bool equals (int, int) throw(); //no exception is thrown from this function

Note that a function that is declared without an exception specification such as

bool equals (int, int);

guarantees nothing about its exceptions: It might throw any exception, or it might throw no exceptions. Exception Specifications Are Enforced At Runtime

An exception specification may not be checked at compile time,

but rather at runtime. When a function attempts to throw an exception that it is not allowed to throw according to its exception specification, the exception handling mechanism detects the violation and invokes the standard function unexpected(). The default behavior of unexpected() is to call terminate(), which terminates the program. A violation of an exception specification is most likely a bug, and should not occur -- this is why the default behavior is program termination. The default behavior can be altered, nonetheless, by using the function set_unexpected().

Because exception specifications are enforced only at runtime, the compiler might deliberately ignore code that seemingly violates exception specifications. Consider the following:

int f();    // no exception specification, f can throw any type of exception
void g(int j) throw()    // g promises not to throw any exception at all
{
  int result = f(); // if f throws an exception, g will violate its guarantee
                    //not to throw an exception. still, this code is legal
}

In this example, the function g(), which is not allowed to throw any exception, invokes the function f(). f(), however, is free to throw any exception because it has no exception specification. If f() throws an exception, it propagates through g(), thereby violating g()'s guarantee not to throw any exception.It might seem surprising that exception specifications are enforced only at runtime because at least some of the violations can be caught at compile time and flagged as errors. This is not the case, however. There are several compelling reasons for the runtime checking policy.. In the preceding example, f() can be a legacy C function. It is impossible to enforce every C function to have an exception specification. Forcing the programmer to write unnecessary try and catch(...) blocks in g() "just in case" is impractical as well -- what if the programmer knows that f() doesn't throw any exception at all and the code is therefore safe? By enforcing exception specifications at runtime, C++ applies the "trust the programmer" policy instead of forcing an unnecessary burden on both the programmer and the implementation.

Concordance of Exception Specification

C++ requires exception specification concordance in derived classes. This means that an overriding virtual function in a derived class has to have an exception specification that is at least as restrictive as the exception specification of the overridden function in the base class. For example

// various exception classes
class BaseEx{};
class DerivedEx: public BaseEx{};
class OtherEx {};
class A
{
public:
  virtual void f() throw (BaseEx);
  virtual void g() throw (BaseEx);
  virtual void h() throw (DerivedEx);
  virtual void i() throw (DerivedEx);
  virtual void j() throw(BaseEx);
};
class D: public A
{
public: 
  void f() throw (DerivedEx); //OK, DerivedEx is derived from BaseEx
class D: public A
{
public: 
  void f() throw (DerivedEx); //OK, DerivedEx is derived from BaseEx
  void g() throw (OtherEx);  //error; exception specification is 
                             //incompatible with A's
  void h() throw (DerivedEx); //OK, identical to the exception 
                              //specification in base
  void i() throw (BaseEx); //error, BaseEx is not a DerivedEx nor is it
                           //derived from DerivedEx
  void j()  throw (BaseEx,OtherEx); //error, less restrictive than the
                                    //specification of A::j
};
};

The same concordance restrictions apply to pointers to functions. A pointer to a function that has an exception specification can be assigned only to a function that has an identical or a more restrictive exception specification. This implies that a pointer to function that has no exception specification cannot be assigned to a function that has one. Note, however, that an exception specification is not considered part of the function type. Therefore, you cannot declare two distinct functions that differ only in their exception specification. For example

void f(int) throw (Y);
void f(int) throw (Z); //error; redefinition of 'void f(int)'

For the same reason, declaring a typedef that contains an exception specification is also an error:

typedef void (*PF) (int) throw(Exception); // error

Exceptions During Object's Construction and Destruction

Constructors and destructors are invoked automatically; in addition, they cannot return values to indicate a runtime error. Seemingly, the most plausible way of reporting runtime errors during object construction and destruction is by throwing an exception. However, there are additional factors that you have to consider before throwing an exception in these cases. You should be particularly cautious about throwing an exception from a destructor.

Throwing Exceptions From A Destructor is Dangerous

Throwing an exception from a destructor is not recommended. The problem is that a destructor might be invoked due to another exception as part of the stack unwinding. If a destructor that was invoked due to another exception also throws an exception of its own, the exception handling mechanism invokes terminate(). If you really have to throw an exception from a destructor, it is advisable to check first whether another uncaught exception is currently being processed.

Checking for an Uncaught Exception

A thrown exception is considered caught when its corresponding handler has been entered (or, if such a handler cannot be found, when the function unexpected() has been invoked). In order to check whether a thrown exception is currently being processed, you can use the standard function uncaught_exception() (which is defined in the standard header <stdexcept>). For example

class FileException{};
File::~File() throw (FileException)
{
   if ( close(file_handle) != success) // failed to close current file?
  {
    if (uncaught_exception()  == true ) // is there any uncaught exception 
                                        //being processed currently?
       return;  // if so, do not throw an exception
    throw FileException(); // otherwise, it is safe to throw an exception
                           // to signal an error
  }
  return; // success
}

Still, a better design choice is to handle exceptions within a destructor rather than let them propagate into the program. For example

void cleanup() throw (int);
class C 
{
public:
  ~C();
};
C::~C() 
{
  try 
  {
    cleanup();
  }
  catch(int) 
  {
    //handle the exception within the destructor
  }
}

If an exception is thrown by cleanup(), it is handled inside the destructor. Otherwise, the thrown exception will propagate outside the destructor, and if the destructor has been invoked while unwinding the stack due to another exception, terminate() will be called.

Global Objects: Construction and Destruction

Conceptually, the construction of global objects takes place before program outset. Therefore, any exception that is thrown from a constructor of a global object can never be caught. This is also true for a global object's destructor -- the destruction of a global object executes after a program's termination. Hence, an exception that is thrown from a global object's destructor cannot be handled either.

Advanced Exception Handling Techniques

The simple try-throw-catch model can be extended even further to handle more complicated runtime errors. This section discusses some of the more advanced uses of exception handling, including exception hierarchies, rethrowing exceptions, function try blocks and the auto_ptr class.

Standard Exceptions

C++ defines a hierarchy of standard exceptions that are thrown at runtime when abnormal conditions arise. The standard exception classes are derived from std::exception (defined in the <stdexcept> header). This hierarchy enables the application to catch these exceptions in a single catch statement:

catch (std::exception& exc)
{
  // handle exception of type std::exception as well as 
  //any exception derived from it
}

The standard exceptions that are thrown by built-in operators of the language are

std::bad_alloc     //by operator new
std::bad_cast     //by operator dynamic_cast < >
std::bad_typeid   //by operator typeid
std::bad_exception   //thrown when an exception specification of 

//a function is violatedAll standard exceptions have provided the member function what(), which returns a const char * with an implementation-dependent verbal description of the exception. Note, however, that the standard library has an additional set of exceptions that are thrown by its components.

Exception Handlers Hierarchy

Exceptions are caught in a bottom-down hierarchy: Specific (most derived classes) exceptions are handled first, followed by groups of exceptions (base classes), and, finally, a catch all handler. For example

#include <stdexcept>  
#include <iostream>
using namespace std;
int main()
{
  try
  {
      char * buff = new char[100000000];
      //...use buff
  }
  catch(bad_alloc& alloc_failure)   // bad_alloc is 
                                         //derived from exception
  {
    cout<<"memory allocation failure";
    //... handle exception thrown by operator new
  }
  catch(exception& std_ex) 
  {
    cout<< std_ex.what() <<endl; 
  }
  catch(...)  // exceptions that are not handled elsewhere are caught here
  {
    cout<<"unrecognized exception"<<endl;
  }
  return 0;
}

Handlers of the most derived objects must appear before the handlers of base classes. This is because handlers are tried in order of appearance. It is therefore possible to write handlers that are never executed, for example, by placing a handler for a derived class after a handler for a corresponding base class. For example

catch(std::exception& std_ex) //bad_alloc exception is always handled here   
{
  //...handle the exception 
}
catch(std::bad_alloc& alloc_failure)   //unreachable 
{
  cout<<"memory allocation failure";
}

Rethrowing an Exception

An exception is thrown to indicate an abnormal state. The first handle to catch the exception can try to fix the problem. If it fails to do so, or if it only manages to perform a partial recovery, it can still rethrow the exception, thereby letting a higher try block handle it. For that purpose, try blocks can be nested in a hierarchical order, enabling a rethrown exception from a lower catch statement to be caught again. A rethrow is indicated by a throw statement without an operand. For example

#include <iostream>
#include <string>
using namespace std;
enum {SUCCESS, FAILURE};
class File
{
  public: File (const char *) {}
  public: bool IsValid() const {return false; }
  public: int OpenNew() const {return FAILURE; }
};
class Exception {/*..*/}; //general base class for exceptions
class FileException: public Exception
{
  public: FileException(const char *p) : s(p) {}
  public: const char * Error() const { return s.c_str(); }
  private: string s;
};
void func(File& );
int main()
{
  try //outer try
  {
    File f ("db.dat");
    func;   // 1
  }
catch(...) // 7
  //this handler will catch the re-thrown exception; 
  //note: the same exception type is required
  {
    cout<<"re-thrown exception caught";
  }
  return 0;
}
void func(File & f)
{
  try //inner try
  {
      if (f.IsValid() == false )
      throw FileException("db.dat");  // 2
  }
  catch(FileException &fe) // 3
//first chance to cope with the exception
  {
    cout<<"invalid file specification" <<fe.Error()<<endl;
    if (f.OpenNew() != SUCCESS) (5)
     //re-throw the original exception and let a higher handler deal with it
    throw; // 6
  }
}

In the preceding example, the function func() is called from the try block inside main() (1). The second try block inside func() throws an exception of type FileException (2). This exception is caught by the catch block inside func() (3). The catch block attempts to remedy the situation by opening a new file. This attempt fails (5), and the FileException is rethrown (6). Finally, the rethrown exception is caught -- this time, by the catch(...) block inside main() (7).

Function try Blocks

A function try block is a function whose body consists of a try block and its associated handlers. A function try block enables a handler to catch an exception

that is thrown during the execution of the initializer expressions in the constructor's member initialization list or during the execution of the constructor's body. Note, however, that unlike handlers of ordinary exceptions, the handler of a function try block merely catches the exception -- it cannot continue the object's construction normally. This is because the partially constructed object is destroyed as a result of the stack unwinding. In addition, the handler of a function try block cannot execute a return statement (eventually, the handler must exit by a throw). What is the use of a function try block then? The handler enables you to throw a different exception than the one that it just caught, thereby preventing a violation of the exception specification. For example

class X{}; 
C::C(const std::string& s) throw (X) //  allowed to throw X only 
try
: str(s) // str's constructor might throw a bad_alloc exception, 
         // might violate C's exception specification
{
  // constructor function body
}
catch (...) //handle any exception thrown from ctor initializer or ctor body
{
  //... 
  throw X(); //replace bad_alloc exception with an exception of type X
}

In this example, a string object is first constructed as a member of class C. string might throw a bad_alloc exception during its construction. The function try block catches the bad_alloc exception and throws instead an exception of type X, which satisfies the exception specification of C's constructor.

Use auto_ptr<> to Avoid Memory Leaks

The Standard Library supplies the class template auto_ptr<> (discussed in Chapter 10, "STL and Generic Programming"), which automatically deallocates memory that is allocated on the free store in much the same manner as local objects are reclaimed in case of exiting their scope. When an auto_ptr<> is instantiated, it can be initialized with a pointer to an object that is allocated on the free store. When the current scope is exited, the destructor of the auto_ptr<> object automatically deletes the object that is bound to it. By using auto_ptr<>, you can avoid memory leakage in the case of an exception. Furthermore, auto_ptr<> can simplify programming by sparing the bother of explicitly deleting objects that were allocated on the free store. auto_ptr<> is defined in the standard <memory> header file.

For example

#include <memory>
#include <iostream>
using namespace std;
class Date{ public: const char * DateString(); };
void DisplayDate()
{
        //create a local object of type auto_ptr<Date>
  auto_ptr<Date> pd (new Date); //now pd is owned by the template object
  cout<< pd-> DateString();
  //pd is automatically deleted by the destructor of auto_ptr; 
}

In the preceding example, the auto_ptr<> instance, pd, can be used like an ordinary pointer to Date. The overloaded operators *, ->, and & of auto_ptr<> provide the pointer-like syntax. pd's bound object is automatically destroyed when DisplayDate() exits.

Exception Handling Performance Overhead

By nature, exception handling relies heavily on runtime type checking. When an exception is thrown, the implementation has to determine whether the exception was thrown from a try block (an exception can be thrown from a program section that is not enclosed within a try block -- by operator new, for example). If indeed the exception was thrown from a try block, the implementation compares the type of the exception and attempts to find a matching handler in the current scope. If a match is found, control is transferred to the handler's body. This is the optimistic scenario. What if the implementation cannot find a matching handler for the exception, though, or what if the exception was not thrown from a try block? In such a case, the current function is unwound from the stack and the next active function in the stack is entered. The same process is reiterated until a matching handler has been found (at that point, all the automatic objects that were created on the path from a try block to a throw expression have been destroyed). When no matching handler can be found in the program, terminate() is invoked and the program terminates.

Additional Runtime Type Information

The exception handling mechanism has to store additional data about the type of every exception object and every catch statement in order to perform the runtime matching between an exception and its matching handler. Because an exception can be of any type, and because it can be polymorphic as well, its dynamic type must be queried at runtime, using runtime type information (RTTI). RTTI, imposes an additional overhead in terms of both execution speed and program size (see Chapter 7, "Runtime Type Information"). Yet RTTI alone is not enough. The implementation also requires runtime code information, that is, information about the structure of each function. This information is needed to determine whether an exception was thrown from a try block. This information is generated by the compiler in the following way: The compiler divides each function body into three parts: one that is outside a try block with no active objects, a second part that is also outside a try block but that has active objects that have to be destroyed during stack unwinding, and a third part that is within a try block.

Toggling Exception Handling Support

The technicalities of exception handling implementation vary among compilers and platforms. In all of them, however, exception handling imposes additional overhead even when no exception is ever thrown. The overhead lies in both execution speed and program size. Some compilers enable you to toggle exception handling support. When it is turned off, the additional data structures, lookup tables, and auxiliary code are not generated. However, turning off exception handling is rarely an option. Even if you do not use exceptions directly, you are probably using them implicitly: Operator new, for example, might throw a std::bad_alloc exception when it fails -- and so do other built-in operators; STL containers might throw their own exceptions, and so might other functions of the Standard Library. Code libraries that are supplied by third party vendors might use exceptions as well. Therefore, you can safely turn off exception handling support only when you are porting pure C code into a C++ compiler. As long as pure C code is used, the additional exception handling overhead is unnecessary and can be avoided.

Misuses of Exception Handling

Exception handling is not confined to errors. Some programmers might use it simply as an alternative control structure to for loops or while and do blocks. For example, a simple application that prompts the user to enter data until a certain condition has been fulfilled can be (rather naively) implemented as follows:

#include <iostream>
using namespace std;
class Exit{}; //used as exception object
int main()
{
 int num;
 cout<< "enter a number; 99 to exit" <<endl;
 try
 {
   while (true) //infinitely
   {
     cin>>num;
     if (num == 99)
         throw Exit(); //exit the loop
     cout<< "you entered: " << num << "enter another number " <<endl;
   }
 }
 catch (Exit& )
 {
   cout<< "game over" <<endl;
 }
 return 0;
}

In the preceding example, the programmer locates an infinite loop within a try block. The throw statement breaks the loop and transfers control to the following catch statement. This style of programming is not recommended, however. It is very inefficient due to the excess overhead of exception handling. Furthermore, it is rather verbose and might have been much simpler and shorter had it been written with a break statement. In demo apps such as this one, the difference is mostly a stylistic one. In large-scale applications, the use of exception handling as an alternative control structure imposes a significant performance overhead.

Simple runtime errors that can be handled safely and effectively without the heavy machinery of exception handling need to also be treated by traditional methods. For example, a password entry dialog box should not throw an exception if the user mistyped his or her password. It is much simpler to redisplay the password entry dialog again with an appropriate error message. On the other hand, if the user enters wrong passwords dozens of times in a row, this can indicate a malicious break-in attempt. In this case, an exception should be thrown. The appropriate handler can page the system administrator and security officer.

Conclusions

The exception handling mechanism of C++ overcomes the problems associated with the traditional methods. It frees the programmer from writing tedious code that checks the success status of every function call. Exception handling also eliminates human mistakes. Another important advantage of exception handling is the automatic unwinding of the stack, which ensures that local active objects are properly destroyed and their resources are released.

Implementing an exception handling mechanism was not a trivial task. The need to query the dynamic type of exception led to the introduction of RTTI into C++. The additional overhead of exception handling derives from the RTTI data structures, the "scaffolding" code that is generated by the compiler, and other implementation-dependent factors. Exceptions can be grouped into categories; the standard exception classes are a good example of this. In recent years, a few loopholes in the exception handling mechanism have been fixed. The first was the addition of exception specifications to functions' prototypes. The second was the introduction of a function try block, which enables the program to handle an exception that is thrown during the execution of the initializer expressions in the constructor's member initialization list or during the execution of the constructor's body.

Exception handling is a very powerful and flexible tool for handling runtime errors effectively. However, use it judiciously.


Contents


© Copyright 1999, Macmillan Computer Publishing. All rights reserved.