The fundamental idea is that a function that finds a problem it cannot cope with, throws an exception, hoping that its (direct or indirect) caller can handle the problem. A function that wants to handle that kind of problem can indicate that it is willing to catch that exception.
This style of error handling compares favorably with more traditional techniques. Consider the alternatives. Upon detecting a problem that cannot be handled locally, the program could:
[1] Terminate the program,
[2] Return a value representing ‘‘error,’’
[3] Return a legal value and leave the program in an illegal state, or
[4] Call a function supplied to be called in case of ‘‘error.’’
Case [1], ‘‘terminate the program,’’ is what happens by default when an exception isn’t caught. For most errors, we can and must do better. In particular, a library that doesn’t know about the purpose and general strategy of the program in which it is embedded cannot simply exit()or abort(). A library that unconditionally terminates cannot be used in a program that cannot afford to crash. One way of viewing exceptions is as a way of giving control to a caller when no meaningful action can be taken locally.
Case [2], ‘‘return an error value,’’ isn’t always feasible because there is often no acceptable ‘‘error value.’’ For example, if a function returns an int, every int might be a plausible result. Even where this approach is feasible, it is often inconvenient because every call must be checked for the error value. This can easily double the size of a program. Consequently, this approach is rarely used systematically enough to detect all errors.
Case [3], ‘‘return a legal value and leave the program in an illegal state,’’ has the problem that the calling function may not notice that the program has been put in an illegal state. For example, many standard C library functions set the global variable errno to indicate an error. However, programs typically fail to test errno consistently enough to avoid consequential errors caused by values returned from failed calls. Furthermore, the use of global variables for recording error conditions doesn’t work well in the presence of concurrency.
Exception handling is not meant to handle problems for which case [4], ‘‘call an error handler function,’’ is relevant. However, in the absence of exceptions, an error handler function has exactly the three other cases as alternatives for how it handles the error.
Exceptions
The exception mechanism is designed to handle only synchronous exceptions, such as array range checks and I/O errors. Asynchronous events, such as keyboard interrupts and certain arithmetic errors, are not necessarily exceptional and are not handled directly by this mechanism. Asynchronous events require mechanisms fundamentally different from exceptions (as defined here) to handle them cleanly and efficiently. Many systems offer mechanisms, such as signals, to deal with asynchrony, but because these tend to be system dependent, lets not discuss them.
Grouping of Exceptions
An exception is an object of some class representing an exceptional occurrence. Code that detects an error (often a library) throws an object. A piece of code expresses desire to handle an exception by a catch clause. The effect of a throw is to unwind the stack until a suitable catch is found (in a function that directly or indirectly invoked the function that threw the exception).
Often, exceptions fall naturally into families. This implies that inheritance can be useful to structure exceptions and to help exception handling. Organizing exceptions into hierarchies can be important for robustness of code.
NOTE: Please note that neither the built in mathematical operations nor the basic math library (shared with C) reports arithmetic errors as exceptions. One reason for this is that detection of some arithmetic errors, such as divide by zero, are asynchronous on many pipelined machine architectures.
Derived Exceptions
The use of class hierarchies for exception handling naturally leads to handlers that are interested only in a subset of the information carried by exceptions. In other words, an exception is typically caught by a handler for its base class rather than by a handler for its exact class. The semantics for catching and naming an exception are identical to those of a function accepting an argument. That is, the formal argument is initialized with the argument value. This implies that the exception thrown is ‘‘sliced’’ to the exception caught.
As always, pointers or references can be used to avoid losing information permanently.
Composite Exceptions
Not every grouping of exceptions is a tree structure. Often, an exception belongs to two groups. This nonhierarchical organization of error handling is important where services.
Catching Exceptions
Consider:
void f()
{
try {
throw E() ;
}
catch(H) {
/ / when do we get here?
}
}
The handler is invoked:
[1] If H is the same type as E.
[2] If H is an unambiguous public base of E.
[3] If H and E are pointer types and [1] or [2] holds for the types to which they refer.
[4] If H is a reference and [1] or [2] holds for the type to which H refers.
In addition, we can add const to the type used to catch an exception in the same way that we can add it to a function parameter. This doesn’t change the set of exceptions we can catch; it only restricts us from modifying the exception caught.
In principle, an exception is copied when it is thrown, so the handler gets hold of a copy of the original exception. In fact, an exception may be copied several times before it is caught. Consequently, we cannot throw an exception that cannot be copied.
The implementation may apply a wide variety of strategies for storing and transmitting exceptions. It is guaranteed, however, that there is sufficient memory to allow new to throw the standard out of memory exception, badalloc.
Re-Throw
Having caught an exception, it is common for a handler to decide that it can’t completely handle the error. In that case, the handler typically does what can be done locally and then throws the exception again. Thus, an error can be handled where it is most appropriate. This is the case even when the information needed to best handle the error is not available in a single place, so that the recovery action is best distributed over several handlers.
catch (Matherr) {
if (canhandleitcompletely) {
/ / handle the Matherr
return;
}
else {
/ / do what can be done here
throw; / / rethrow
the exception
}
A re-throw is indicated by a throw without an operand. If a re-throw is attempted when there is no exception to re-throw, terminate()will be called. A compiler can detect and warn about some, but not all, such cases.
Catch Every Exception
A degenerate version of this catch and re-throw technique can be important. As for functions, the ellipsis ... indicates ‘‘any argument’’, so catch(...) means ‘‘catch any exception.’’
Order of Handlers
Because a derived exception can be caught by handlers for more than one exception type, the order in which the handlers are written in a try statement is significant. The handlers are tried in order.
Resource Management
When a function acquires a resource – that is, it opens a file, allocates some memory from the free store, sets an access control lock, etc., – it is often essential for the future running of the system that the resource be properly released. Often that ‘‘proper release’’ is achieved by having the function that acquired it release it before returning to its caller.
The code using the resource is enclosed in a try block that catches every exception, releases the resource, and re-throws the exception.
The problem with this solution is that it is verbose, tedious, and potentially expensive. Furthermore, any verbose and tedious solution is error prone because programmers get bored. Fortunately, there is a more elegant solution. The general form of the problem looks like this:
void acquire()
{
/ / acquire resource 1
/ / ...
/ / acquire resource n
/ / use resources
/ / release resource n
/ / ...
/ / release resource 1
}
It is typically important that resources are released in the reverse order of their acquisition. This strongly resembles the behavior of local objects created by constructors and destroyed by destructors. Thus, we can handle such resource acquisition and release problems by a suitable use of objects of classes with constructors and destructors.
The destructor should be called independently of whether the function is exited normally or exited because an exception is thrown.
Using Constructors and Destructors
The technique for managing resources using local objects is usually referred to as ‘‘resource acquisition is initialization.’’ This is a general technique that relies on the properties of constructors and destructors and their interaction with exception handling.
An object is not considered constructed until its constructor has completed. Then and only then will stack unwinding call the destructor for the object. An object composed of sub-objects is constructed to the extent that its sub-objects have been constructed. An array is constructed to the extent that its elements have been constructed (and only fully constructed elements are destroyed during unwinding).
A constructor tries to ensure that its object is completely and correctly constructed. When that cannot be achieved, a well written constructor restores – as far as possible – the state of the system to what it was before creation. Ideally, naively written constructors always achieve one of these alternatives and don’t leave their objects in some ‘‘half constructed’’ state. This can be achieved by applying the ‘‘resource acquisition is initialization’’ technique to the members.
Auto_ptr
The standard library provides the template class autoptr, which supports the ‘‘resource acquisition
is initialization’’ technique.
template class std: :autoptr
{
templatestruct autoptrref{ /* ... */ }; / / helper class
X* ptr;
public:
typedef X elementtype;
explicit autoptr(X* p =0) throw() { ptr=0; }
autoptr(autoptr& a) throw() { ptr=a.ptr; a.ptr=0; } / / note: not const autoptr&
template autoptr(autoptr& a) throw() { ptr=a.ptr; a.ptr=0; }
autoptr& operator=(autoptr& a) throw() { ptr=a.ptr; a.ptr=0; }
template autoptr& operator=(autoptr& a) throw() { ptr=a.ptr; a.ptr=0; }
~autoptr() throw() { delete ptr; }
X& operator*() const throw() { return *ptr; }
X* operator>()
const throw() { return ptr; }
X* get() const throw() { return ptr; } / / extract pointer
X* release() throw() { X* t = ptr; ptr=0; return t; } / / relinquish ownership
void reset(X* p =0) throw() { if (p!=ptr) { delete ptr; ptr=p; } }
autoptr(autoptrref) throw() ; / / copy from autoptrref
template operator autoptrref() throw() ; / / copy from autoptrref
template operator autoptr() throw() ; / / destructive copy from autoptr
};
Exceptions and New
What happens if X´s constructor throws an exception? Is the memory allocated by the operator new() freed? For the ordinary case, the answer is yes, so the initializations of X’s members don’t cause memory leaks. When the placement syntax is used, the answer cannot be that simple.
Resource Exhaustion
A recurring programming problem is what to do when an attempt to acquire a resource fails. When confronted with such problems, programmers come up with two styles of solutions:
Resumption: Ask some caller to fix the problem and carry on.
Termination: Abandon the computation and return to some caller.
In C++, the resumption model is supported by the function call mechanism and the termination model is supported by the exception handling mechanism. Both can be illustrated by a simple implementation and use of the standard library operator new().
void* operator new(sizet size)
{
for (;;) {
if (void* p =malloc(size)) return p; / / try to find memory
if (newhandler == 0) throw badalloc() ; / / no handler: give up
newhandler() ; / / ask for help
}
}
If memory is found, operator new() can return a pointer to it. Otherwise, operator new()calls the newhandler.
The newhandler used in the implementation of operator new()is a pointer to a function maintained by the standard function setnewhandler(). If I want mynewhandler() to be used as the newhandler, I say:
setnewhandler(&mynewhandler) ;
In general, it is wise to organize resource allocation in layers (levels of abstraction) and avoid having one layer depend on help from the layer that called it. Experience with larger systems shows that successful systems evolve in this direction.
Throwing an exception requires an object to throw. A C++ implementation is required to have enough spare memory to be able to throw badalloc in case of memory exhaustion. However, it is possible that throwing some other exception will cause memory exhaustion.
Exceptions in Constructors
Exceptions provide a solution to the problem of how to report errors from a constructor. Because a constructor does not return a separate value for a caller to test, the traditional (that is, non exception handling) alternatives are:
[1] Return an object in a bad state, and trust the user to test the state.
[2] Set a nonlocal variable (e.g., errno) to indicate that the creation failed, and trust the user to test that variable.
[3] Don’t do any initialization in the constructor, and rely on the user to call an initialization function before the first use.
[4] Mark the object ‘‘uninitialized’’ and have the first member function called for the object do the real initialization, and that function can then report an error if initialization fails.
Exception handling allows the information that a construction failed to be transmitted out of the
constructor.
other words, the fundamental aim of the error handling techniques is to pass information about an error from the original point of detection to a point where there is sufficient information available to recover from the problem, and to do so reliably and conveniently.
The ‘‘resource acquisition is initialization’’ technique is the safest and most elegant way of handling constructors that acquire more than one resource. In essence, the technique reduces the problem of handling many resources to repeated application of the (simple) technique for handling one resource.
Exceptions and Member Initialization
What happens if a member initializer (directly or indirectly) throws an exception? By default, the exception is passed on to whatever invoked the constructor for the member’s class. However, the constructor itself can catch such exceptions by enclosing the complete function body – including the member initializer list – in a tryblock.
X: :X(int s)
try
:v(s) / / initialize v by s
{
/ / ...
}
catch (Vector: :Size) { / / exceptions thrown for v are caught here
/ / ...
}
Copy constructors are special in that they are invoked implicitly and because they often both acquire and release resources. In particular, the standard library assumes proper – non exception throwing – behavior of copy constructors. For these reasons, care should be taken that a copy constructor throws an exception only in truly disastrous circumstances. Complete recovery from an exception in a copy constructor is unlikely to be feasible in every context of its use. To be even potentially safe, a copy constructor must leave behind two objects, each of which fulfills the invariant of its class.
Naturally, copy assignment operators should be treated with as much care as copy constructors.
Exceptions in Destructors
From the point of view of exception handling, a destructor can be called in one of two ways:
[1] Normal call: As the result of a normal exit from a scope, a delete, etc.
[2] Call during exception handling: During stack unwinding, the exception handling mechanism exits a scope containing an object with a destructor.
In the latter case, an exception may not escape from the destructor itself. If it does, it is considered a failure of the exception handling mechanism and std::terminate()is called. After all, there is no general way for the exception handling mechanism or the destructor to determine whether it is acceptable to ignore one of the exceptions in favor of handling the other.
The standard library function uncaughtexception()returns true if an exception has been thrown but hasn’t yet been caught. This allows the programmer to specify different actions in a destructor depending on whether an object is destroyed normally or as part of stack unwinding.
Exceptions That Are Not Errors
If an exception is expected and caught so that it has no bad effects on the behavior of the program, then how can it be an error? Only because the programmer thinks of it as an error and of the exception handling mechanisms as tools for handling errors. Alternatively, one might think of the exception handling mechanisms as simply another control structure.
This actually has some charm, so it is a case in which it is not entirely clear what should be considered an error and what should not.
Using exceptions as alternate returns can be an elegant technique.
However, such use of exceptions can easily be overused and lead to obscure code. Whenever reasonable, one should stick to the ‘‘exception handling is error handling’’ view. When this is done, code is clearly separated into two categories: ordinary code and error handling code.
Exception Specifications
Throwing or catching an exception affects the way a function relates to other functions. It can therefore be worthwhile to specify the set of exceptions that might be thrown as part of the function declaration. For example:
void f(int a) throw (x2, x3) ;
This specifies that f()may throw only exceptions x2, x3, and exceptions derived from these types, but no others. When a function specifies what exceptions it might throw, it effectively offers a guarantee to its callers. If during execution that function does something that tries to abrogate the guarantee, the attempt will be transformed into a call of std::unexpected(). The default meaning of unexpected() is std::terminate(), which in turn normally calls abort().
The most important advantage is that the function declaration belongs to an interface that is visible to its callers. Function definitions, on the other hand, are not universally available. Even when we do have access to the source code of all our libraries, we strongly prefer not to have to look at it very often. In addition, a function with an exception specification is shorter and clearer than the equivalent handwritten version.
A function declared without an exception specification is assumed to throw every exception. For example:
int f() ; / / can throw any exception
A function that will throw no exceptions can be declared with an empty list:
int g() throw () ; / / no exception thrown
Checking Exception Specifications
It is not possible to catch every violation of an interface specification at compile time. However, much compile time checking is done. The way to think about exception specifications is to assume that a function will throw any exception it can. The rules for compile time checking exception specifications outlaw easily detected absurdities.
If any declaration of a function has an exception specification, every declaration of that function (including the definition) must have an exception specification with exactly the same set of exception types.
A virtual function may be overridden only by a function that has an exception specification at least as restrictive as its own (explicit or implicit) exception specification.
class B {
public:
virtual void f() ; / / can throw anything
virtual void g() throw(X,Y) ;
virtual void h() throw(X) ;
};
class D : public B {
public:
void f() throw(X) ; / / ok
void g() throw(X) ; / / ok: D::g() is more restrictive than B::g()
void h() throw(X,Y) ; / / error: D::h() is less restrictive than B::h()
};
you can assign a pointer to function that has a more restrictive exception specification to a pointer to function that has a less restrictive exception specification, but not vice versa. For example:
void f() throw(X) ;
void (*pf1)() throw(X,Y) = &f; / / ok
void (*pf2)()throw() = &f; / / error: f() is less restrictive than pf2
In particular, you cannot assign a pointer to a function without an exception specification to a pointer to function that has one.
Mapping Exceptions
Occasionally, the policy of terminating a program upon encountering an unexpected exception is too Draconian. In such cases, the behavior of unexpected()must be modified into something acceptable. The simplest way of achieving that is to add the standard library exception std::badexception to an exception specification.
In that case, unexpected()will simply throw badexception instead of invoking a function to try to cope. For example:
class X{ };
class Y{ };
void f() throw(X,std: :badexception)
{
/ / ...
throw Y() ; / / throw ‘‘bad’’ exception
}
The exception specification will catch the unacceptable exception Y and throw an exception of type badexception instead.
There is actually nothing particularly bad about badexception; it simply provides a mechanism that is less drastic than calling terminate(). However, it is still rather crude. In particular, information about which exception caused the problem is lost.
Uncaught Exceptions
If an exception is thrown but not caught, the function std::terminate()will be called. The terminate() function will also be called when the exception handling mechanism finds the stack corrupted and when a destructor called during stack unwinding caused by an exception tries to exit using an exception.
An unexpected exception is dealt with by the unexpectedhandler determined by setunexpected(). Similarly, the response to an uncaught exception is determined by an
uncaughthandler set by std::setterminate()from:
typedef void(*terminatehandler)() ;
terminatehandler setterminate(terminatehandler) ;
The return value is the previous function given to setterminate().
The reason for terminate()is that exception handling must occasionally be abandoned for less subtle error handling techniques. For example, terminate()could be used to abort a process or maybe to reinitialize a system. The intent is for terminate()to be a drastic measure to be applied when the error recovery strategy implemented by the exception handling mechanism has failed and it is time to go to another level of a fault tolerance strategy.
An uncaughthandler is assumed not to return to its caller. If it tries to, terminate()will call abort(). Note that abort()indicates abnormal exit from the program. The function exit()can be used to exit a program with a return value that indicates to the surrounding system whether the exit is normal or abnormal
If you want to ensure cleanup when an uncaught exception happens, you can add a catch-all handler to main()in addition to handlers for exceptions you really care about.
Advice
[1] Use exceptions for error handling.
[2] Don’t use exceptions where more local control structures will suffice.
[3] Use the ‘‘resource allocation is initialization’’ technique to manage resources.
[4] Not every program needs to be exception safe.
[5] Use ‘‘resource allocation is initialization’’ and exception handlers to maintain invariants.
[6] Minimize the use of try blocks. Use ‘‘resource acquisition is initialization’’ instead of explicit handler code.
[7] Not every function needs to handle every possible error.
[8] Throw an exception to indicate failure in a constructor.
[9] Avoid throwing exceptions from copy constructors.
[10] Avoid throwing exceptions from destructors.
[11] Have main()catch and report all exceptions.
[12] Keep ordinary code and error handling code separate.
[13] Be sure that every resource acquired in a constructor is released when throwing an exception in that constructor.
[14] Keep resource management hierarchical.
[15] Use exception specifications for major interfaces.
[16] Beware of memory leaks caused by memory allocated by new not being released in case of an exception.
[17] Assume that every exception that can be thrown by a function will be thrown.
[18] Don’t assume that every exception is derived from class exception.
[19] A library shouldn’t unilaterally terminate a program. Instead, throw an exception and let a caller decide.
[20] A library shouldn’t produce diagnostic output aimed at an end user. Instead, throw an exception and let a caller decide.
[21] Develop an error handling strategy early in a design.
This style of error handling compares favorably with more traditional techniques. Consider the alternatives. Upon detecting a problem that cannot be handled locally, the program could:
[1] Terminate the program,
[2] Return a value representing ‘‘error,’’
[3] Return a legal value and leave the program in an illegal state, or
[4] Call a function supplied to be called in case of ‘‘error.’’
Case [1], ‘‘terminate the program,’’ is what happens by default when an exception isn’t caught. For most errors, we can and must do better. In particular, a library that doesn’t know about the purpose and general strategy of the program in which it is embedded cannot simply exit()or abort(). A library that unconditionally terminates cannot be used in a program that cannot afford to crash. One way of viewing exceptions is as a way of giving control to a caller when no meaningful action can be taken locally.
Case [2], ‘‘return an error value,’’ isn’t always feasible because there is often no acceptable ‘‘error value.’’ For example, if a function returns an int, every int might be a plausible result. Even where this approach is feasible, it is often inconvenient because every call must be checked for the error value. This can easily double the size of a program. Consequently, this approach is rarely used systematically enough to detect all errors.
Case [3], ‘‘return a legal value and leave the program in an illegal state,’’ has the problem that the calling function may not notice that the program has been put in an illegal state. For example, many standard C library functions set the global variable errno to indicate an error. However, programs typically fail to test errno consistently enough to avoid consequential errors caused by values returned from failed calls. Furthermore, the use of global variables for recording error conditions doesn’t work well in the presence of concurrency.
Exception handling is not meant to handle problems for which case [4], ‘‘call an error handler function,’’ is relevant. However, in the absence of exceptions, an error handler function has exactly the three other cases as alternatives for how it handles the error.
Exceptions
The exception mechanism is designed to handle only synchronous exceptions, such as array range checks and I/O errors. Asynchronous events, such as keyboard interrupts and certain arithmetic errors, are not necessarily exceptional and are not handled directly by this mechanism. Asynchronous events require mechanisms fundamentally different from exceptions (as defined here) to handle them cleanly and efficiently. Many systems offer mechanisms, such as signals, to deal with asynchrony, but because these tend to be system dependent, lets not discuss them.
Grouping of Exceptions
An exception is an object of some class representing an exceptional occurrence. Code that detects an error (often a library) throws an object. A piece of code expresses desire to handle an exception by a catch clause. The effect of a throw is to unwind the stack until a suitable catch is found (in a function that directly or indirectly invoked the function that threw the exception).
Often, exceptions fall naturally into families. This implies that inheritance can be useful to structure exceptions and to help exception handling. Organizing exceptions into hierarchies can be important for robustness of code.
NOTE: Please note that neither the built in mathematical operations nor the basic math library (shared with C) reports arithmetic errors as exceptions. One reason for this is that detection of some arithmetic errors, such as divide by zero, are asynchronous on many pipelined machine architectures.
Derived Exceptions
The use of class hierarchies for exception handling naturally leads to handlers that are interested only in a subset of the information carried by exceptions. In other words, an exception is typically caught by a handler for its base class rather than by a handler for its exact class. The semantics for catching and naming an exception are identical to those of a function accepting an argument. That is, the formal argument is initialized with the argument value. This implies that the exception thrown is ‘‘sliced’’ to the exception caught.
As always, pointers or references can be used to avoid losing information permanently.
Composite Exceptions
Not every grouping of exceptions is a tree structure. Often, an exception belongs to two groups. This nonhierarchical organization of error handling is important where services.
Catching Exceptions
Consider:
void f()
{
try {
throw E() ;
}
catch(H) {
/ / when do we get here?
}
}
The handler is invoked:
[1] If H is the same type as E.
[2] If H is an unambiguous public base of E.
[3] If H and E are pointer types and [1] or [2] holds for the types to which they refer.
[4] If H is a reference and [1] or [2] holds for the type to which H refers.
In addition, we can add const to the type used to catch an exception in the same way that we can add it to a function parameter. This doesn’t change the set of exceptions we can catch; it only restricts us from modifying the exception caught.
In principle, an exception is copied when it is thrown, so the handler gets hold of a copy of the original exception. In fact, an exception may be copied several times before it is caught. Consequently, we cannot throw an exception that cannot be copied.
The implementation may apply a wide variety of strategies for storing and transmitting exceptions. It is guaranteed, however, that there is sufficient memory to allow new to throw the standard out of memory exception, badalloc.
Re-Throw
Having caught an exception, it is common for a handler to decide that it can’t completely handle the error. In that case, the handler typically does what can be done locally and then throws the exception again. Thus, an error can be handled where it is most appropriate. This is the case even when the information needed to best handle the error is not available in a single place, so that the recovery action is best distributed over several handlers.
catch (Matherr) {
if (canhandleitcompletely) {
/ / handle the Matherr
return;
}
else {
/ / do what can be done here
throw; / / rethrow
the exception
}
A re-throw is indicated by a throw without an operand. If a re-throw is attempted when there is no exception to re-throw, terminate()will be called. A compiler can detect and warn about some, but not all, such cases.
Catch Every Exception
A degenerate version of this catch and re-throw technique can be important. As for functions, the ellipsis ... indicates ‘‘any argument’’, so catch(...) means ‘‘catch any exception.’’
Order of Handlers
Because a derived exception can be caught by handlers for more than one exception type, the order in which the handlers are written in a try statement is significant. The handlers are tried in order.
Resource Management
When a function acquires a resource – that is, it opens a file, allocates some memory from the free store, sets an access control lock, etc., – it is often essential for the future running of the system that the resource be properly released. Often that ‘‘proper release’’ is achieved by having the function that acquired it release it before returning to its caller.
The code using the resource is enclosed in a try block that catches every exception, releases the resource, and re-throws the exception.
The problem with this solution is that it is verbose, tedious, and potentially expensive. Furthermore, any verbose and tedious solution is error prone because programmers get bored. Fortunately, there is a more elegant solution. The general form of the problem looks like this:
void acquire()
{
/ / acquire resource 1
/ / ...
/ / acquire resource n
/ / use resources
/ / release resource n
/ / ...
/ / release resource 1
}
It is typically important that resources are released in the reverse order of their acquisition. This strongly resembles the behavior of local objects created by constructors and destroyed by destructors. Thus, we can handle such resource acquisition and release problems by a suitable use of objects of classes with constructors and destructors.
The destructor should be called independently of whether the function is exited normally or exited because an exception is thrown.
Using Constructors and Destructors
The technique for managing resources using local objects is usually referred to as ‘‘resource acquisition is initialization.’’ This is a general technique that relies on the properties of constructors and destructors and their interaction with exception handling.
An object is not considered constructed until its constructor has completed. Then and only then will stack unwinding call the destructor for the object. An object composed of sub-objects is constructed to the extent that its sub-objects have been constructed. An array is constructed to the extent that its elements have been constructed (and only fully constructed elements are destroyed during unwinding).
A constructor tries to ensure that its object is completely and correctly constructed. When that cannot be achieved, a well written constructor restores – as far as possible – the state of the system to what it was before creation. Ideally, naively written constructors always achieve one of these alternatives and don’t leave their objects in some ‘‘half constructed’’ state. This can be achieved by applying the ‘‘resource acquisition is initialization’’ technique to the members.
Auto_ptr
The standard library provides the template class autoptr, which supports the ‘‘resource acquisition
is initialization’’ technique.
template
{
template
X* ptr;
public:
typedef X elementtype;
explicit autoptr(X* p =0) throw() { ptr=0; }
autoptr(autoptr& a) throw() { ptr=a.ptr; a.ptr=0; } / / note: not const autoptr&
template
autoptr& operator=(autoptr& a) throw() { ptr=a.ptr; a.ptr=0; }
template
~autoptr() throw() { delete ptr; }
X& operator*() const throw() { return *ptr; }
X* operator>()
const throw() { return ptr; }
X* get() const throw() { return ptr; } / / extract pointer
X* release() throw() { X* t = ptr; ptr=0; return t; } / / relinquish ownership
void reset(X* p =0) throw() { if (p!=ptr) { delete ptr; ptr=p; } }
autoptr(autoptrref
template
template
};
Exceptions and New
What happens if X´s constructor throws an exception? Is the memory allocated by the operator new() freed? For the ordinary case, the answer is yes, so the initializations of X’s members don’t cause memory leaks. When the placement syntax is used, the answer cannot be that simple.
Resource Exhaustion
A recurring programming problem is what to do when an attempt to acquire a resource fails. When confronted with such problems, programmers come up with two styles of solutions:
Resumption: Ask some caller to fix the problem and carry on.
Termination: Abandon the computation and return to some caller.
In C++, the resumption model is supported by the function call mechanism and the termination model is supported by the exception handling mechanism. Both can be illustrated by a simple implementation and use of the standard library operator new().
void* operator new(sizet size)
{
for (;;) {
if (void* p =malloc(size)) return p; / / try to find memory
if (newhandler == 0) throw badalloc() ; / / no handler: give up
newhandler() ; / / ask for help
}
}
If memory is found, operator new() can return a pointer to it. Otherwise, operator new()calls the newhandler.
The newhandler used in the implementation of operator new()is a pointer to a function maintained by the standard function setnewhandler(). If I want mynewhandler() to be used as the newhandler, I say:
setnewhandler(&mynewhandler) ;
In general, it is wise to organize resource allocation in layers (levels of abstraction) and avoid having one layer depend on help from the layer that called it. Experience with larger systems shows that successful systems evolve in this direction.
Throwing an exception requires an object to throw. A C++ implementation is required to have enough spare memory to be able to throw badalloc in case of memory exhaustion. However, it is possible that throwing some other exception will cause memory exhaustion.
Exceptions in Constructors
Exceptions provide a solution to the problem of how to report errors from a constructor. Because a constructor does not return a separate value for a caller to test, the traditional (that is, non exception handling) alternatives are:
[1] Return an object in a bad state, and trust the user to test the state.
[2] Set a nonlocal variable (e.g., errno) to indicate that the creation failed, and trust the user to test that variable.
[3] Don’t do any initialization in the constructor, and rely on the user to call an initialization function before the first use.
[4] Mark the object ‘‘uninitialized’’ and have the first member function called for the object do the real initialization, and that function can then report an error if initialization fails.
Exception handling allows the information that a construction failed to be transmitted out of the
constructor.
other words, the fundamental aim of the error handling techniques is to pass information about an error from the original point of detection to a point where there is sufficient information available to recover from the problem, and to do so reliably and conveniently.
The ‘‘resource acquisition is initialization’’ technique is the safest and most elegant way of handling constructors that acquire more than one resource. In essence, the technique reduces the problem of handling many resources to repeated application of the (simple) technique for handling one resource.
Exceptions and Member Initialization
What happens if a member initializer (directly or indirectly) throws an exception? By default, the exception is passed on to whatever invoked the constructor for the member’s class. However, the constructor itself can catch such exceptions by enclosing the complete function body – including the member initializer list – in a tryblock.
X: :X(int s)
try
:v(s) / / initialize v by s
{
/ / ...
}
catch (Vector: :Size) { / / exceptions thrown for v are caught here
/ / ...
}
Copy constructors are special in that they are invoked implicitly and because they often both acquire and release resources. In particular, the standard library assumes proper – non exception throwing – behavior of copy constructors. For these reasons, care should be taken that a copy constructor throws an exception only in truly disastrous circumstances. Complete recovery from an exception in a copy constructor is unlikely to be feasible in every context of its use. To be even potentially safe, a copy constructor must leave behind two objects, each of which fulfills the invariant of its class.
Naturally, copy assignment operators should be treated with as much care as copy constructors.
Exceptions in Destructors
From the point of view of exception handling, a destructor can be called in one of two ways:
[1] Normal call: As the result of a normal exit from a scope, a delete, etc.
[2] Call during exception handling: During stack unwinding, the exception handling mechanism exits a scope containing an object with a destructor.
In the latter case, an exception may not escape from the destructor itself. If it does, it is considered a failure of the exception handling mechanism and std::terminate()is called. After all, there is no general way for the exception handling mechanism or the destructor to determine whether it is acceptable to ignore one of the exceptions in favor of handling the other.
The standard library function uncaughtexception()returns true if an exception has been thrown but hasn’t yet been caught. This allows the programmer to specify different actions in a destructor depending on whether an object is destroyed normally or as part of stack unwinding.
Exceptions That Are Not Errors
If an exception is expected and caught so that it has no bad effects on the behavior of the program, then how can it be an error? Only because the programmer thinks of it as an error and of the exception handling mechanisms as tools for handling errors. Alternatively, one might think of the exception handling mechanisms as simply another control structure.
This actually has some charm, so it is a case in which it is not entirely clear what should be considered an error and what should not.
Using exceptions as alternate returns can be an elegant technique.
However, such use of exceptions can easily be overused and lead to obscure code. Whenever reasonable, one should stick to the ‘‘exception handling is error handling’’ view. When this is done, code is clearly separated into two categories: ordinary code and error handling code.
Exception Specifications
Throwing or catching an exception affects the way a function relates to other functions. It can therefore be worthwhile to specify the set of exceptions that might be thrown as part of the function declaration. For example:
void f(int a) throw (x2, x3) ;
This specifies that f()may throw only exceptions x2, x3, and exceptions derived from these types, but no others. When a function specifies what exceptions it might throw, it effectively offers a guarantee to its callers. If during execution that function does something that tries to abrogate the guarantee, the attempt will be transformed into a call of std::unexpected(). The default meaning of unexpected() is std::terminate(), which in turn normally calls abort().
The most important advantage is that the function declaration belongs to an interface that is visible to its callers. Function definitions, on the other hand, are not universally available. Even when we do have access to the source code of all our libraries, we strongly prefer not to have to look at it very often. In addition, a function with an exception specification is shorter and clearer than the equivalent handwritten version.
A function declared without an exception specification is assumed to throw every exception. For example:
int f() ; / / can throw any exception
A function that will throw no exceptions can be declared with an empty list:
int g() throw () ; / / no exception thrown
Checking Exception Specifications
It is not possible to catch every violation of an interface specification at compile time. However, much compile time checking is done. The way to think about exception specifications is to assume that a function will throw any exception it can. The rules for compile time checking exception specifications outlaw easily detected absurdities.
If any declaration of a function has an exception specification, every declaration of that function (including the definition) must have an exception specification with exactly the same set of exception types.
A virtual function may be overridden only by a function that has an exception specification at least as restrictive as its own (explicit or implicit) exception specification.
class B {
public:
virtual void f() ; / / can throw anything
virtual void g() throw(X,Y) ;
virtual void h() throw(X) ;
};
class D : public B {
public:
void f() throw(X) ; / / ok
void g() throw(X) ; / / ok: D::g() is more restrictive than B::g()
void h() throw(X,Y) ; / / error: D::h() is less restrictive than B::h()
};
you can assign a pointer to function that has a more restrictive exception specification to a pointer to function that has a less restrictive exception specification, but not vice versa. For example:
void f() throw(X) ;
void (*pf1)() throw(X,Y) = &f; / / ok
void (*pf2)()throw() = &f; / / error: f() is less restrictive than pf2
In particular, you cannot assign a pointer to a function without an exception specification to a pointer to function that has one.
Mapping Exceptions
Occasionally, the policy of terminating a program upon encountering an unexpected exception is too Draconian. In such cases, the behavior of unexpected()must be modified into something acceptable. The simplest way of achieving that is to add the standard library exception std::badexception to an exception specification.
In that case, unexpected()will simply throw badexception instead of invoking a function to try to cope. For example:
class X{ };
class Y{ };
void f() throw(X,std: :badexception)
{
/ / ...
throw Y() ; / / throw ‘‘bad’’ exception
}
The exception specification will catch the unacceptable exception Y and throw an exception of type badexception instead.
There is actually nothing particularly bad about badexception; it simply provides a mechanism that is less drastic than calling terminate(). However, it is still rather crude. In particular, information about which exception caused the problem is lost.
Uncaught Exceptions
If an exception is thrown but not caught, the function std::terminate()will be called. The terminate() function will also be called when the exception handling mechanism finds the stack corrupted and when a destructor called during stack unwinding caused by an exception tries to exit using an exception.
An unexpected exception is dealt with by the unexpectedhandler determined by setunexpected(). Similarly, the response to an uncaught exception is determined by an
uncaughthandler set by std::setterminate()from
typedef void(*terminatehandler)() ;
terminatehandler setterminate(terminatehandler) ;
The return value is the previous function given to setterminate().
The reason for terminate()is that exception handling must occasionally be abandoned for less subtle error handling techniques. For example, terminate()could be used to abort a process or maybe to reinitialize a system. The intent is for terminate()to be a drastic measure to be applied when the error recovery strategy implemented by the exception handling mechanism has failed and it is time to go to another level of a fault tolerance strategy.
An uncaughthandler is assumed not to return to its caller. If it tries to, terminate()will call abort(). Note that abort()indicates abnormal exit from the program. The function exit()can be used to exit a program with a return value that indicates to the surrounding system whether the exit is normal or abnormal
If you want to ensure cleanup when an uncaught exception happens, you can add a catch-all handler to main()in addition to handlers for exceptions you really care about.
Advice
[1] Use exceptions for error handling.
[2] Don’t use exceptions where more local control structures will suffice.
[3] Use the ‘‘resource allocation is initialization’’ technique to manage resources.
[4] Not every program needs to be exception safe.
[5] Use ‘‘resource allocation is initialization’’ and exception handlers to maintain invariants.
[6] Minimize the use of try blocks. Use ‘‘resource acquisition is initialization’’ instead of explicit handler code.
[7] Not every function needs to handle every possible error.
[8] Throw an exception to indicate failure in a constructor.
[9] Avoid throwing exceptions from copy constructors.
[10] Avoid throwing exceptions from destructors.
[11] Have main()catch and report all exceptions.
[12] Keep ordinary code and error handling code separate.
[13] Be sure that every resource acquired in a constructor is released when throwing an exception in that constructor.
[14] Keep resource management hierarchical.
[15] Use exception specifications for major interfaces.
[16] Beware of memory leaks caused by memory allocated by new not being released in case of an exception.
[17] Assume that every exception that can be thrown by a function will be thrown.
[18] Don’t assume that every exception is derived from class exception.
[19] A library shouldn’t unilaterally terminate a program. Instead, throw an exception and let a caller decide.
[20] A library shouldn’t produce diagnostic output aimed at an end user. Instead, throw an exception and let a caller decide.
[21] Develop an error handling strategy early in a design.
No comments:
Post a Comment