C++异常处理之三


Exception Handling Support

When an exception is thrown, the compilation system must do the following:

  1. Examine the function in which the throw occurred.
  2. Determine if the throw occurred in a try block.
  3. If so, then the compilation system must compare the type of the exception against the type of each catch clause.
  4. If the types match, control must pass to the body of the catch clause.
  5. If either it is not within a try block or none of the catch clauses match, then the system must (a) destruct any active local objects, (b) unwind the current function from the stack, and (c) go to the next active function on the stack and repeat items 2?.

Determine if the Throw Occurred within a try Block

A function, recall, can be thought of as a set of regions:

  • A region outside a try block with no active local objects
  • A region outside a try block but with one or more active local objects requiring destruction
  • A region within an active try block

The compiler needs to mark off these regions and make these markings available to the runtime EH system. A predominant strategy for doing this is the construction of program counter-range tables.

Recall that the program counter holds the address of the next program instruction to be executed. To mark off a region of the function within an active try block, the beginning and ending program counter value (or the beginning program counter value and its range value) can be stored in a table.

When a throw occurs, the current program counter value is matched against the associated range table to determine whether the region active at the time is within a try block. If it is, the associated catch clauses need to be examined (we look at this in the next subsection). If the exception is not handled (or if it is rethrown), the current function is popped from the program stack and the value of the program counter is restored to the value of the call site and the cycle begins again.

Compare the Type of the Exception against the Type of Each Catch Clause

For each throw expression, the compiler must create a type descriptor encoding the type of the exception. If the type is a derived type, the encoding must include information on all of its base class types. (It’s not enough to simply encode the public base class types because the exception could be caught by a member function; within the scope of a member function, conversion between a derived and nonpublic base class is permitted.)

The type descriptor is necessary because the actual exception is handled at runtime when the object itself otherwise has no type information associated with it. RTTI is a necessary side effect of support for EH. (I look further at RTTI in Section 7.3.)

The compiler must also generate a type descriptor for each catch clause. The runtime exception handler compares the type descriptor of the object thrown with that of each catch clause’s type descriptor until either a match is found or the stack has been unwound and terminate() invoked.

An exception table is generated for each function. It describes the regions associated with the function, the location of any necessary cleanup code (invocation of local class object destructors), and the location of catch clauses if a region is within an active try block.

What Happens When an Actual Object Is Thrown during Program Execution?

When an exception is thrown, the exception object is created and placed generally on some form of exception data stack. Propagated from the throw site to each catch clause are the address of the exception object, the type descriptor (or the address of a function that returns the type descriptor object associated with the exception type), and possibly the address of the destructor for the exception object, if one if defined.

Consider a catch clause of the form

catch( exPoint p ) 
{ 
   // do something 
   throw; 
} 

and an exception object of type exVertex derived from exPoint. The two types match and the catch clause block becomes active. What happens with p?

  • p is initialized by value with the exception object the same as if it were a formal argument of a function. This means a copy constructor and destructor, if defined or synthesized by the compiler, are applied to the local copy.
  • Because p is an object and not a reference, the non-exPoint portion of the exception object is sliced off when the values are copied. In addition, if virtual functions are provided for the exception hierarchy, the vptr of p is set to exPoint’s virtual table; the exception object’s vptr is not copied.

What happens when the exception is rethrown? Is p now the object propagated or the exception object originally generated at the throw site? p is a local object destroyed at the close of the catch clause. Throwing p would require the generation of another temporary. It also would mean losing the exVertex portion of the original exception. The original exception object is rethrown; any modifications to p are discarded.

A catch clause of the form

catch( exPoint &rp ) 
{ 
   // do something 
   throw; 
} 

refers to the actual exception object. Any virtual invocations resolve to the instances active for exVertex, the actual type of the exception object. Any changes made to the object are propagated to the next catch clause.

Finally, here is an interesting puzzle. If we have the following throw expression:

exVertex errVer; 

// ... 
mumble() 
{ 
   // ... 
   if ( mumble_cond ) { 
      errVer.fileName( "mumble()" ); 
      throw errVer; 
   } 
   // ... 
} 

Is the actual exception errVer propagated or is a copy of errVer constructed on the exception stack and propagated? A copy is constructed; the global errVer is not propagated. This means that any changes made to the exception object within a catch clause are local to the copy and are not reflected within errVer. The actual exception object is destroyed only after the evaluation of a catch clause that does not rethrow the exception.


《“C++异常处理之三”》 有 1 条评论

  1. 对于每个 throw 表达式,编译器必须创建一个对异常类型进行编码的类型描述符。如果该类型是派生类型,则编码必须包含有关其所有基类类型的信息。(仅仅对公共基类类型进行编码是不够的,因为异常可能会被成员函数捕获;在成员函数的范围内,允许在派生基类和非公共基类之间进行转换。)

    意思就是说,在代码进行throw的时候,编译器需要为这个throw的类型进行编码,而且要能够知道具体的类型。如果这个异常类型是一个派生类型,比如有这样的继承关系:
    class A : public B,private C, protected D
    A是从B,C,D多重继承而来,
    那么编译器要知道A是从B,C,D继承而来的,不能仅仅知道是从B继承而来的。原话的意思就是说,不能仅仅编码B,C和D也需要编码。
    然后接下来的意思就是说,因为异常可能在A的成员函数中被捕获,并且可以在成员函数中将异常转换为B或者C,D等。

发表回复

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