C++异常处理之二


A Quick Review of Exception Handling

Exception handling under C++ consists of the following three main syntactic components:

  1. A throw clause. A throw clause raises an exception at some point within the program. The exception thrown can be of a built-in or user-defined type.
  2. One or more catch clauses. Each catch clause is the exception handler. It indicates a type of exception the clause is prepared to handle and gives the actual handler code enclosed in braces.
  3. A try block. A try block surrounds a sequence of statements for which an associated set of catch clauses is active.

When an exception is thrown, control passes up the function call sequence until either an appropriate catch clause is matched or main() is reached without a handler’s being found, at which point the default handler, terminate(), is invoked. As control passes up the call sequence, each function in turn is popped from the program stack (this process is called unwinding the stack). Prior to the popping of each function, the destructors of the function’s local class objects are invoked.

What is slightly nonintuitive about EH is the impact it has on functions that seemingly have nothing to do with exceptions. For example, consider the following:

1. Point* 
2. mumble() 
3. { 
4.    Point *pt1, *pt2; 
5.    pt1 = foo(); 
6.    if ( !pt1 ) 
7.      return 0; 
8. 
9.    Point p; 
10. 
11.   pt2 = foo(); 
12.   if ( !pt2 ) 
13.       return pt1; 
14. 
15.      ... 
16 } 

If an exception is thrown within the first call of foo() (line 5), then the function can simply be popped from the program stack. The statement is not within a try block, so there is no need to attempt to match up with a catch clause; nor are there any local class objects requiring destruction. If an exception is thrown within the second call of foo() (line 11), however, then the EH mechanism must invoke the destructor for p before unwinding the function from the program stack.

Under EH, that is, lines 4? and lines 9?6 are viewed as semantically distinct regions of the function with differing runtime semantics should an exception be thrown. Moreover, EH support requires additional bookkeeping. An implementation could either associate the two regions with separate lists of local objects to be destroyed (these would be set up at compile time) or share a single list that is added to and shrunk dynamically at runtime.

On the programmer level, EH also alters the semantics of functions that manage resources. The following function includes, for example, both a locking and unlocking of a shared memory region and is no longer guaranteed to run correctly under EH even though it seemingly has nothing to do with exceptions:

void 
mumble( void *arena ) 
{ 
   Point *p = new Point; 
   smLock( arena ); // function call 

   // problem if an exception is thrown here 
   // ... 

   smUnLock( arena ); // function call 
   delete p; 
} 

In this case, the EH facility views the entire function as a single region requiring no processing other than unwinding the function from the program stack. Semantically, however, we need to both unlock shared memory and delete p prior to the function being popped. The most straightforward (if not the most effective) method of making the function “exception proof” is to insert a default catch clause, as follows:

void 
mumble( void *arena ) 
{ 
   Point *p; 
   p = new Point; 
   try { 
      smLock( arena ); 
      // ... 
   } 
   catch ( ... ) { 
      smUnLock( arena ); 
      delete p; 
      throw; 
   } 

   smUnLock( arena ); 
   delete p; 
} 

The function now has two regions:

  1. The region outside the try block for which there is nothing for the EH mechanism to do except pop the program stack
  2. The region within the try block (and its associated default catch clause)

Notice that the invocation of operator new is not within the try block. Is this an error on my part? If either operator new or the Point constructor invoked after the allocation of memory should throw an exception, neither the unlocking of memory nor the deletion of p following the catch clause is invoked. Is this the correct semantics?

Yes, it is. If operator new throws an exception, memory from the heap would not have been allocated and the Point constructor would not have been invoked. So, there would be no reason to invoke operator delete. If, however, the exception were thrown within the Point constructor following allocation from the heap, any constructed composite or subobject within Point (that is, a member class or base class object) would automatically be destructed and then the heap memory freed. In either case, there is no need to invoke operator delete. (I revisit this at the end of this section.)

Similarly, if an exception were thrown during the processing of operator new, the shared memory segment addressed by arena would never have become locked; therefore, there would be no need to unlock it.

The recommended idiom for handling these sorts of resource management is to encapsulate the resource acquisition within a class object, the destructor of which frees the resource (this style becomes cumbersome, however, when resources need to be acquired, released, then reacquired and released a second or subsequent times):

void 
mumble( void *arena ) 
{ 
   auto_ptr <Point> ph ( new Point ); 
   SMLock sm( arena ); 

   // no problem now if an exception is thrown here 
   // ... 

   // no need to explicitly unlock & delete 
   // local destructors invoked here 
   // sm.SMLock::~SMLock(); 
   // ph.auto_ptr <Point>::~auto_ptr <Point> () 
} 

The function now has three regions with regard to EH:

  1. One in which the standard auto_ptr is defined
  2. One in which the SMLock is defined
  3. One that follows the two definitions and spans the entire function

If an exception is thrown within the auto_ptr constructor, there are no active local objects for the EH mechanism to destroy. If, however, an exception is thrown within the SMLock constructor, the auto_ptr object must be destroyed prior to the unwinding of the program stack. Within the third region, of course, both local objects must be destroyed.

EH support complicates the constructors of classes with member class and base class subobjects with constructors. A class that is partially constructed must apply the destructors for only these subobjects and/or member objects that have been constructed. For example, if a class X has member objects A, B, and C, each with a constructor/destructor pair, then if A’s constructor throws an exception, neither A, B, nor C needs its destructor invoked. If B’s constructor throws an exception, A’s destructor needs to be invoked, but not C’s. Providing for all these contingencies is the compiler’s responsibility.

Similarly, if the programmer writes

// class Point3d : public Point2d { ... }; 
Point3d *cvs = new Point3d[ 512 ]; 

this is what happens:

  1. Memory is allocated for the 512 Point3d objects from the heap.
  2. If (1) succeeds, the Point2d constructor then Point3d constructor is applied on each element in turn.

What if the Point3d constructor for element 27 throws an exception? For element 27, only the Point2d destructor needs to be applied. For the first 26 constructed elements, both the Point3d and Point2d destructors need to be applied. Then the memory allocated must be freed.


发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注