深度探索c++对象模型之异常处理的前介

      对于exception handling【异常处理】来说,编译器的主要工作是找出所有的catch子句,以处理被丢出来的exception。这就需要追踪程序堆栈中的每一个函数的当前作用域,包括追踪函数中的local class object当时的情况。在此同时,编译器还得提供某种查询exception object的方法,以知道它的真正类型,这就导致了执行器的RTTI【类型识别】。最后,还需要某种机制用来管理被丢出去的exception object,包括它们的产生、存储、可能的解构【如果有相关的destructor】、clean up以及一般存取。也可能有一个以上的object同时起作用。一般来所,exception handling机制、编译器所产生的数据结构以及执行期的一个exception library这三者要紧密协作。在程序的大小和执行速度之间,编译器要有所取舍:

1):如果要追求执行速度,编译器可以在编译期建立用于支持exception的数据结构,这样会使编译后的程序代码膨胀起来,但编译器可以忽略这些结构,直到有exception被丢出来。

2):如果要追求编译后程序简洁,编译器可以在执行期再建立数据结构,这也许会拖累程序的执行速度,之所以是也许,因为编译器只有在必要的时候才建立那些数据结构。

      过去有一种错误的看法,认为由于exception handling的出现才导致了cfront的灭绝,因为不可能提供一个即可接受而又强固的exception handling机制,却没有支持程序代码产生器和链接器。UNIX Software Laboratory(USL)当初搁置了由HP移交出来的exception handling C-generating implementation大约有一年之久,但最后USL终于一直赞成取消掉cfront 4.0和其它更高版本的开发计划。


      c++的exception handling主要由3个语汇组件构成:

1):一个throw子句,它从程序某处发出一个exception,被丢出去的exception可以是内建类型,也可以是用户的自定义类型。

2):若干个catch子句,每一个catch子句都是一个exception handler,它用来表示说,这个子句准备处理某种类型的exception,并且在封闭的大括号区段中提供实际的处理代码。

3):一个try区段,它被围绕以一系列的叙述句(statements),这些叙述句可能引发catch子句起作用。

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


      Exception handling中不那么符合直觉的是它对于没什么事做的函数所带来的影响,比如下面这个例子:

Point * mumble()
{
  Point *ptr1,*ptr2;
  ptr1 = foo();
  if( !ptr1 )
    return 0;

  Point p; //注意这行代码

  ptr2 = foo();
  if( !ptr2 )
    return ptr1;

  ...
}

如果有一个exception在第一次调用foo()【第5行代码】被丢出,那么整个mumble函数就会被推出堆栈,但由于调用foo()的操作并不在try区段内,也就不需要尝试和catch子句配合,在这里也没有任何的local class object会被析构。但是,如果exception是在第二次调用foo()【第11行代码】被丢出,exception handling机制就必须在从程序堆栈中“unwinding”【解开】mumble函数之前,把类对象p给析构调用【调用p的destructor】。

      在exception handling机制之下,第4行~第8行和第9行~第16行会被当成两块语义不同的区域,因为当exception被抛出来时,这两块区域有着不同的执行期语义。而且,要想支持exception handling,需要额外的一些登记操作与数据,编译器的做法有两种,第一种是把两块区域以个别的将被摧毁的local object链表【在编译期已经备好】联合起来,第二种做法是让两块区域共享同一块链表,该链表会在执行期扩大或缩小。

      在用户层面,exception handling也改变了函数在资源管理上的语义,例如,下面的函数中含有对一块共享内存的locking与unlocking操作,虽然看起来和exception handling没什么关联,但在exception handling之下并不能保证正确运行:

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

//如果有一个exception在这里出现,问题就来了
//...

smUnLock(arena);
delete p;
}
在本例之中,exception handling机制把整个函数视为单一区域,无需操心将函数从程序堆栈中unwinding的事情。然而从语义上来说,在函数被推出堆栈之前,我们需要unlock共享内存,并delete p,让函数成为“exception proof”最明确的方法就是安插一个default catch子句,像这样:
void
mumble(void *arena)
{
  Point *p;
  p = new Point;
  try{
    smLock( arena );
    //...
  }
  catch(..){
    smUnLock( arena );
    delete p;
    throw;
  }

  smUnLock( arena );
  delete p;
}

于是这个函数现在有了两个区域:

1、try block以外的区域,在那里,exception handling机制除了pop程序堆栈之外,并没有其他事要做。

2、try block以内的区域,以及它所联合的default catch子句。

      请注意,new运算符的调用并非在try区段内,这是错误吗?如果new运算符或是Point constructor在配置内存之后发生一个exception,那么内存既不会被unlocking,p也不会被delete【这两个动作都在catch区段内】,这是正确的语义吗?

     是的,这是正确的语义:如果new运算符丢出一个exception,那么就不需要配置堆中的内存,Point constructor也不会被调用,所以更不需要delete p;就算是在Point constructor中发生了exception,此时的内存已经配置完成,那么Point之中任何构造好的对象或者子对象都将被自动解构掉,然后堆内存也会被释放掉。无论是它们的哪一种情况,都不需要调用delete运算符。同样的道理,如果一个exception是在new运算符执行过程中被丢出,arena所指向的内存就绝不会被locked,所以自然不需要unlock。

      处理这些资源管理问题,有一个方法是,将资源需求封装在一个类对象体内,并且由destructor来释放资源。

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

  //如果在这里丢出一个exception,现在就没有问题了
  //...

  //不需要明确的unlock和delete
  //sm.SMLock::~SMLock();
  //ph.auto_ptr<Point>::auto_ptr<Point>();
}
从exception handling的角度来看,上面这个函数有三个区域:

1、第一区段是auto_ptr被定义之处。

2、第二区段是SMLock被定义之处。

3、上述两个定义之后的整个函数。

      如果exception是在auto_ptr constructor中被丢出的,那么就没有active local object需要被exception handling机制摧毁;但是如果SMLock constructor中丢出一个exception,则auto_ptr object必须在uinwinding之前被摧毁;最后在第3个区段中,两个object objects当然都要被摧毁。

      支持exception handling机制,会使那些拥有类成员子对象和基类子对象的类们的构造器更加复杂。一个类如果被部分构造,那么它的destructor只能析构那些已经构造出来的子对象或成员对象,比如说,如果class X有成员对象类A、类B和类C【它们都有自己的构造器和析构器】,如果A的构造器丢出一个exception,那么A、B和C都不需要调用它们的destructor,但如果在构造B的时候丢出一个exception,那么就必须要调用A的destructor了;处理这些问题,是编译器的责任。
      所以说,如果有以下代码:
//Point3d继承自Point2d
Point3d *p3d = new Point3d[100];

那么这段代码在执行期会发生两件事:首先,从堆中分配出100个Point3d数组大小的内存;如果上面的步骤成功,先是执行Point2d的构造器,再接着执行Point3d的构造器,它们会作用于每一个元素身上。

      但如果第27个Point3d元素执行构造器时丢出一个exception呢?那么对这个第27号单个对象元素而言,只需执行它父类的destructor,即Point2d的析构器。但是对于前面26个对象元素而言,无论Point3d还是Point2d,它们的destructor都需要被调用,然后内存统统被回收。


对exception handling机制的支持

      当一个exception被丢出时,编译系统必须做以下事情:

1):检验发生throw操作的函数。

2):决定throw操作是否发生在try区段之内。

3):如果是,编译系统必须把exception type拿出来和每一个catch子句相比较。

4):如果某个catch子句吻合了,那么程序控制交给该catch子句。

5):如果throw的发生并不在try区段之内,或者没有任何catch子句相吻合,那么编译系统只能做三件事:要么摧毁所有的active local object【活跃本地对象】;要么从堆栈中把当前的函数unwind掉;要么进行到程序堆栈中的下一个函数中去,然后重复步骤2)~5)。

      

      一个函数可以被分成好几个区域:

1):try区段之外,但没有active local objects【活跃本地对象】;

2):还是try区段之外,但有一个或更多的active local objects。

3):try区段以内。

      编译器必须标识出以上各个区域,并使它们对执行期的exception handling机制有所帮助。一个很好的策略的就是构造出program counter-range。

      program counter-range,在Intel CPU中被称为EIP缓存器,它内含下一个即将被执行的程序指令。为了在一个内含try区段的函数中标示出某个区域,可以把program counter的起始值和结束值储存在一个表格中。

      当throw操作发生时,当前的program counter值会被拿出来与对应的范围表格进行比较,用来决定当前作用中的区域是否在一个try区段之内。如果是,就需要找出相关联的catch子句,如果这个exception无法被处理,或者它再次被丢出,当前的这个函数就会从程序堆栈中被poped(被推出),而program counter会被设定为调用端地址,然后这样的循环再次开始。


将exception type和每一个catch子句的类型作比较

      对于每一个被丢出的exception,编译器必须产生一个类型描述器,对exception的类型进行编码。如果这是一个deriver type,那么编码信息必须包括这个派生类中所有的基类的类型信息。只编进public base class的类型是不够的,因为这个exception可能被一个members function捕捉,而在一个member function的范围之中,在deriver class和nonpublic base class之间可以转换。

      类型描述器【type descriptor】是必要的,因为真正的exception是在执行期被处理,其object必须有自己的类型信息,RTTI正是因为支持exception handling而获得的副产品。

编译器还必须为每一个catch子句产生类型描述器,执行期的exception handling会对“被丢出的object的类型描述器”和“每一个catch子句的类型描述器”进行比较——要么找到一个吻合的catch子句,要么堆栈被unwind而且terminate()被调用。


当一个实际对象在程序执行时被丢出,会发生什么事?

当一个exception被丢出时,exception object会被产生出来并放在相同形式的exception数据堆栈中,从throw端传递给catch子句的是exception object的地址、类型描述器(或者是一个函数指针,该函数会返回与该exception type有关的类型描述对象),以及可能会有的exception object描述器(如果用户定义了的话)。

考虑一个catch子句定义如下:

catch(exPoint p)
{
  ...//做些事。。。
  throw;
}
以及一个exception object,类型为exVertex,派生自exPoint。这两种类型都吻合,那么catch子句就会起作用。那么p会发生什么事呢?

p将以exception object作为初值,就像一个函数参数一样,这意味着如果定义了一个copy constructor和一个destructor的话,它们都会作用于local copy之上。

如果p是一个object而不是一个reference,当它的内容被拷贝的时候,这个exception object的non-exPoint部分会被切割掉,此外,如果为了exception的继承而提供了虚拟函数,那么p的vptr会被设定为exPoint的virtual table,exceptin object的vptr不会被拷贝。

当这个exception被再一次丢出去时,会发生什么事呢?p现在是繁殖出来的object,还是从throw端产生的原始exception handling?p是一个local object,在catch子句的末端将被摧毁。丢出p需要产生另一个临时对象,并丧失原来的exception的exVertex部分,原来的exception object被再一次丢出;任何对p的修改都会被放弃。

像下面的catch子句:

catch( exPoint &rp )
{
  //...
  throw;
}
则参考到真正的exception object,任何虚拟调用都会被resolved【决议】为instances active of exVertex,即exception object的真正类型,任何对此object的改变都会被繁殖到下一个catch子句中去。

最后,让我们来看下一个例子:

exVertex errVer;

//...
memble()
{
  //...
  if(memble_cond){
    errVer.fileName( "memble()" );
    throw errVer;
  }
  //...
}
上面的代码中,究竟是真正的exception errVer被繁殖,还是一个errVer的复制品被构造于exception stack之中并被繁殖?答案是后者,全局性的errVer并没有被繁殖,这意味着在catch子句中对于exception object的任何改变都是局部性的,不会影响到errVer本身,只有在一个catch子句评估完毕并且知道它不会再丢出exception之后,真正的exception才会被摧毁。



      

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值