站在对象模型的顶端——异常处理

前言

 
想要支持exception handling,编译器的主要任务是找出catch子句,以处理被丢出来的exception。这需要追踪程序堆栈中的每一个函数的当前作用区域。同时,编译器必须提供某种查询exception objects的方法,以了解其实际类型(这直接引发了RTTI的出现)。最后,还需要某种机制用以管理被丢出的object。一般而言,exception handling机制需要与编译器所产生的数据结构以及执行期的一个exception library紧密合作。在程序大小和执行速度之间,编译期必须有所抉择:

  • 维持执行速度
    编译器可以在编译时期建立起用于支持的数据结构,这会使程序代码膨胀。
  • 维持程序大小
    编译器在执行期建立用于支持的数据结构,这会影响程序的执行效率。

Exception Handling快速检阅

 
C++的exception handling由三个主要的词汇组件构成:

  1. 一个throw子句。它在程序某处发出一个exception,被丢出的exception可以是内建类型,也可以是用户自定义类型。
  2. 一个或多个catch子句,每一个catch子句都是一个exception handler。其含义在于表明某子句处理某种类型的exception,并且在封闭区段中提供实际处理程序。
  3. 一个try区段,这个区段中的内容可能会引发catch子句起作用。

当一个exception被丢出时,控制权会从函数调用中被释放出来,并寻找一个吻合的catch子句。如果不存在吻合者,那么默认的处理例程terminate()会被调用。当控制权被放弃后,堆栈中的每一个函数调用也就被推离,这个程序称为unwinding the stack。在每一个函数被推离堆栈之前,函数的local class objects的destructor会被调用。

问题实例

 
Exception handling会给函数带来一定的影响,考虑下述函数:

1
2
3
4
5
6
7
8
Point* mumble(){
Point *pt1,*pt2;
pt1=foo();
if(!pt1) return nullptr;
Point p;
pt2=foo();
if(!pt2) return pt1;
}

如果有一个exception在第一次调用foo()时被抛出,那么这个mumble()函数会被推出程序堆栈。由于本函数不存在任何try语句,因此不会有任何catch子句需要和他结合,不过幸运地是这里也没有任何local class object需要析构。如果有一个exception在第二次调用foo()时被抛出,exception handling机制就必须在从程序堆栈中unwinding这个函数之前,先调用p的destructor。

在Exception handling之下,L2~L4以及L5~L7被视为两个语意不同的区域,因为当exception被抛出时,这两个区域存在不同的执行期语意。
(接下来作者探讨了exception对资源的影响,并且引出了RAII的概念)


对Exception Handling的支持

 
当一个exception发生时,编译系统必须完成以下事情:

  1. 检验发生throw操作的函数
  2. 判断throw操作是否发生在try语句中
  3. 如果在try中,则进行catch子句匹配
  4. 若发生匹配,则将流程控制转交catch子句
  5. 如果3、4并没有符合
    那么系统必须摧毁所有active local object,从堆栈中unwind当前函数,并且进行到程序堆栈中的下一个函数,重复2-5操作。

判断throw操作是否在try中

之前曾经介绍过,一个函数可以被划分为很多区域:

  • try区段以外的区域,而且没有active local objects
  • try区段以外的区域,但存在active local objects需要析构
  • try区段以内的区域

编译器必须标示上述区域的一个有效策略是构造 program counter-range表格。

program counter(Intel CPU中为EIP缓存器)内含一个即将执行的程序指令,为了在一个内含try区段的函数中标示出某个区域,可以把program counter的起始值和结束值存储在一个表格中。当throw操作发生时,当前的program counter值将会与表格内部存储值进行比对,从而确定当前是否在try区段中,从而进行下一步操作。

catch子句匹配

对于每一个被抛出的exception,编译器必须产生一个类型描述器,对exception的类型进行编码。如果那是一个derived type,则编码内容必须涵盖其所有base class的类型信息。只编入public base class的信息是不够的,因为这个exception可能被一个member function捕获,而在一个member function的scope中,derived class与nonpublic base class之间允许相互转换。

由于exception handling发生在执行期,因此我们必然需要RTTI。编译器还必须为每一个catch子句产生一个类型描述器,它们会试图去与exception的类型描述器匹配,直到吻合,或者调用terminate()。

抛出实际对象带来的后果

当一个exception被丢出时,exception object会被产生出来并置于相同形式的exception数据堆栈中,从throw端传播至catch子句的是exception object的地址、类型描述器等等。

考虑一个catch子句如下:

1
2
3
4
catch(exPoint p){
//do sth
throw;
}

并且有一个exception object,其类型为exVertex,派生自exPoint。当前p会发生什么?

  • p将以exception object作为初值。
    如果存在copy constructor或destructor,将会发生一次拷贝构造
  • 发生slice
    由于p是一个object而非reference,所以在copy过程中发生了slice off.p的vtbl将被设为exPoint的vtbl,vptr不会发生改变。

如果我们再次抛出p,此时我们将建立一个临时对象,并且exVertex的性质将完全丢失。(本小节内容可参见More effective C++ 关于异常处理的几篇)