《深度探索C++对象模型》阅读笔记 第七章 站在对象模型的尖端


在这里插入图片描述

在这里插入图片描述

Template的“实例化”行为(Template Instantiation)

然而,member functions(至少对于那些未被使用过的)不应该被“实例化”。只有在member functions被使用的时候,C++ Standard才要求它们被“实例化”。目前的编译器并不精确遵循这项要求。之所以由使用者来主导“实例化”(instantiation)规则,有两个主要原因:

  1. 空间和时间效率的考虑。如果class中有100个member functions,但你的程序只针对某个类型使用其中两个,针对另一个类型使用其中五个,那么将其他 193个函数都“实例化”将会花费大量的时间和空间。
  2. 尚未实现的机能。并不是一个template实例化的所有类型就一定能够完整支持一组 member functions所需要的所有运算符。如果只“实例化”那些真正用到的member functions,template就能够支持那些原本可能会造成编译时期错误的类型(types)。

可以明确的要求在一个文件中将整个类模板实例化:

template class Point3d<float>;

也可以显示指定实例化一个模板类的成员函数:

template float Point3d<float>::X() const;

或是针对一个模板函数:

template Point3d<float> operator+(   
    const Point3d<float>&, const Point3d<float>& );

模板的错误报告,使用模板并遇到错误的大概都深有体会,那就是一个灾难。

Template的错误报告(Error Reporting within a Template)

所以在一个parsing策略之下,所有语汇(lexing)错误和解析(parsing)错误都会在处理template声明的过程中被标示出来。所有与类型有关的检验,如果牵涉到template参数,都必须延迟到真正的实例化操作(instantiation)发生,才得为之。
目前的编译器,面对一个template声明,在它被一组实际参数实例化之前,只能施行以有限的错误检查。template中那些与语法无关的错误,程序员可能认为十分明显,编译器却让它通过了,只有在特定实例被定义之后,才会发出抱怨。这是目前实现技术上的一个大问题。
在这里插入图片描述

Template中的名称决议法(Name Resolution within a Template)

Template之中,对于一个nonmember name 的决议结果,是根据这个name的使用是否与“用以实例化该template的参数类型”有关而决定的。如果其使用互不相关,那么就以“scope of the template declaration”来决定name。如果其使用互有关联,那么就以“scope of the tem plate instantiation”来决定name。
如例子:

// scope of the template definition
extern double foo(double);
template <class type>
class ScopeRules {
public:
    void invariant() {
        _member = foo(_val);
    }
    type type_dependent() {
        return foo(_member);
    }
    // ...
private:
    int _val;
    type _member;
};
// scope of the template instantiation
extern int foo(int);
// ...
ScopeRules<int> sr0;
// scope of the template instantiation
sr0.invariant();

问: 在invariant()中调用的究竟是哪一个foo()函数实体呢?

// invariant()
void invariant() {
        _member = foo(_val);
    }
//并未影响用以实例化该template的参数类型(就是和模板实例化无关),所以采用
//scope of the template declaration 的 extern double foo(double);

type type_dependent() {
        return foo(_member);
    }
//这就影响了用以实例化该template的参数类型,因为返回值为模板类型,所以采用的是 
//scope of the template instantiation
//便是
extern int foo(int);

Member Function的实例化行为

在这里插入图片描述

在这里插入图片描述

如果一个 virtual function被具现(instantiated)出来,其具现点紧跟在其class的具现点之后。

异常处理

欲支持exception handling,编译器的主要工作就是找出catch子句,以处理被抛出来的 exception。这多少需要追踪程序堆栈中的每一个函数的目前作用区域(包括追踪函数中local class objects当时的情况)。同时,编译器必须提供某种查询exception objects 的方法,以知道其实际类型(这直接导致某种形式的执行期类型识别,也就是 RTTI)。最后,还需要某种机制用以管理被抛出的object,包括它的产生、存储、可能的析构(如果有相关的destructor)、清理(clean up)以及一般存取。也可能有一个以上的objects同时起作用。一般而言,exception handling机制需要与编译器所产生的数据结构以及执行期的一个exception library紧密合作。在程序大小和执行速度之间,编译器必须有所抉择:

  • 为了维护执行速度,编译器可以在编译时期建立起用于支持的数据结构。这会使程序的大小发生膨胀,但编译器可以几乎忽略这些结构,直到有个exception被抛出来。
  • 为了维护程序大小,编译器可以在执行期建立起用于支持的数据结构。这会影响程序的执行速度,但意味着编译器只有在必要的时候才建立那些数据结构(并且可以抛弃之)。

Exception Handling

快速检阅C++的exception handling由三个主要的语汇组件构成:

  1. 一个throw子句。它在程序某处发出一个exception。被抛出去的exception可以是内建类型,也可以是使用者自定类型。
  2. 一个或多个catch子句。每一个catch子句都是一个exception handler。它用来表示说,这个子句准备处理某种类型的exception,并且在封闭的大括号区段中提供实际的处理程序
  3. 一个try区段。它被围绕以一系列的叙述句(statements),这些叙述句可能会引发catch子句起作用。

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

异常抛出有可能带来一些问题,比方在一块内存的lock和unlock内存之间,或是在new和delete之间的代码抛出了异常,那么将导致本该进行的unlock或delete操作不能进行。解决方法之一是:

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

在函数被出栈之前,先截住异常,在unlock和delete之后再将异常原样抛出。new expression的调用不用包括在try块之内是因为,不论在new operator调用时还是构造函数调用时抛出异常,都会在抛出异常之前释放已分配好的资源,所以不用再调用delete 。

另一个办法是,将这些资源管理的问题,封装在一个类对象中,由析构函数释放资源,这样就不需要对代码进行上面那样的处理——利用函数释放控制权之前会析构所有局部对象的原理。

在对单个对象构造过程中抛出异常,会只调用已经构造好的base class object或member class object的析构函数。同样的道理,适用于数组身上,如果在调用构造函数过程中抛出异常,那****么之前所有被构造好的元素的析构函数被调用,对于抛出异常的该元素,则遵循关于单个对象构造的原则,然后释放已经分配好的内存。

只有在一个catch子句评估完毕并且知道它不会再抛出exception后,真正的exception object才会被释放。关于 catch子句使用引用还是使用对象来捕获异常。

对Exception Handling的支持

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

  1. 检验发生throw操作的函数。
  2. 决定throw操作是否发生在try区段中。
  3. 若是,编译系统必须把exception type拿来和每一个catch子句进行比较。
  4. 如果比较后吻合,流程控制应该交到catch子句手中。
  5. 如果throw的发生并不在 try区段中,或没有一个catch子句吻合,那么系统必须(a)摧毁所有active local objects,(b)从堆栈中将目前的函数“unwind”掉,(c)进行到程序堆栈的下一个函数中去,然后重复上述步骤 2~5。

决定throw操作是否发生在try区段中

一个函数可以被想象为好几个区域:

  • try区段以外的区域,而且没有active local objects。
  • try区段以外的区域,但有一个(或以上)的active local objects需要析构。
  • try区段以内的区域。

编译器必须标示出以上各区域,并使它们对执行期的exception handling系统有所作用。一个很棒的策略就是构造出program counter-range表格。
回忆一下,program counter内含下一个即将执行的程序指令。好,为了在一个内含try区段的函数中标示出某个区域,可以把program counter的起始值和结束值(或是起始值和范围)存储在一个表格中。
当throw操作发生时,目前的program counter值被拿来与对应的“范围表格”进行比对,以决定目前作用中的区域是否在一个try区段中。如果是,就需要找出相关的catch子句。如果这个exception无法被处理(或者它被再次抛出),目前的这个函数会从程序堆栈中被推出(popped),而program counter会被设定为调用端地址,然后这样的循环再重新开始。

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

**对于每一个被抛出来的exception,编译器必须产生一个类型描述器,对exception的类型进行编码。**如果那是一个derived type,编码内容必须包括其所有base class的类型信息。只编进public base class的类型是不够的,因为这个exception可能被一个member function捕捉,而在一个member function的范围(scope)之中,derived class和nonpublic base class之间可以转换。
类型描述器(type descriptor)是必要的,因为真正的exception是在执行期被处理的,其object必须有自己的类型信息。RTTI正是因为支持EH而获得的副产品。
编译器还必须为每一个catch子句产生一个类型描述器。执行期的exception handler会将“被抛出之object的类型描述器”和“每一个cause子句的类型描述器”进行比较,直到找到吻合的一个,或是直到堆栈已经被“unwound”而 terminate()已被调用。
每一个函数会产生出一个exception表格,它描述与函数相关的各区域、任何必要的善后处理代码(cleanup code,被local class object destructors调用)以及catch子句的位置(如果某个区域是在try区段之中的话)。

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

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

当catch的参数是基类时,可以捕获其派生类异常。

如果参数是引用类型,则会有多态性质;如果是普通基类,则会被裁剪为基类对象。
如果函数体内修改该捕获对象且继续抛出,对引用类型的修改会被繁殖到下一个catch子句;而非引用类型的修改会被抛弃。

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

执行期类型识别(Runtime Type Identification RTTI)

  1. downcast(向下转换)

意为 将基类转换至其派生类中的某一个。
需要downcast的情况:指向派生类对象的基类指针,需要使用派生类的非虚函数,此时需要先把基类指针转换为派生类指针,再调用函数。
downcast引发的问题:如果downcast不正确的使用,比如:被转换的指针并不是指向该派生类对象,而是指向其他派生类对象或基类对象,则也会被转换且不会报错,后续使用会造成问题。

  1. dynamic_cast运算符

dynamic_cast运算符,用于将 基类指针或引用安全地转换为派生类的指针或引用。

通过在执行期(因此,只对支持多态的类适用)查询指向对象的类型来判断转换是否安全:

如果指针的转换是不安全的,则传回0;

如果引用的转换是不安全的,因为无法将0传回引用,所以会抛出bad_cast exception。

什么是dynamic_cast的真正成本呢?pfct的一个类型描述器会被编译器产生出来。由pt所指向的class object类型描述器必须在执行期通过vptr取得。type_info是C++Standard所定义的类型描述器的class名称,该class中放置着待索求的类型信息。virtual table的第一个slot内含type_info object 的地址;此type_info object与pt所指的class type有关。这两个类型描述器被交给一个runtime library函数,比较之后告诉我们是否吻合。

References并不是Pointers

程序执行中对一个class指针类型施以dynamic_cast 运算符,会获得true或false:

  • 如果传回真正的地址,则表示这一object的动态类型被确认了,一些与类型有关的操作现在可以施行于其上。
  • 如果传回0,则表示没有指向任何object,意味着应该以另一种逻辑施行于这个动态类型未确定的object身上。

在这里插入图片描述

因此当dynamic_cast运算符施行于一个reference 时,不能够提供对等于指针情况下的那一组true/false。取而代之的是,会发生下列事情:

  • 如果reference真正参考到适当的derived class(包括下一层或下下一层,或下下下一层或……),downcast会被执行而程序可以继续进行。
  • 如果reference并不真正是某一种derived class,那么,由于不能够传回0,因此抛出一个 bad_cast exception。
  1. typeid运算符

使用 typeid 运算符,就有可能以一个reference达到相同的执行期替代路线(runtime“alternative pathway”)。typeid运算符传回一个const reference,类型为type_info。如果两个type_info objects相等,这个equality运算符就传回true。

所有类型都可以使用typeid:

对于多态类型,执行期查询虚函数表;非多态类型,直接静态取得。
在这里插入图片描述

Point3d x;
Point *p=&x;
typeid(*p).name();//返回Point3d类型,通过查询虚函数表

因此,可以先使用typeid判断对象类型是否一样,再决定是否转换,以代替dynamic_cast运算符。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值