读书笔记《深度探索c++对象模型》(7) - 模板、异常处理、RTTI
一、模板
- 当编译器发现模板声明时,不会做任何动作,纵使内部有静态数据成员也不可用;仅当被明确指定类型时才可用。即:仅仅只有模板被实例化时才会被产生;同样的类的函数模板也是一样的,仅当该函数被调用具化时才会生成。其主要的原因为:时间和空间的效率问题,即仅实例化必要的某些类型的成员函数或实例化用到的那些成员函数,但最终还是要看编译器如何处理了。
- 模板实例化时,不同的类型意味着不同的实例。
- 因为存在编译器的处理策略,未被具化的模板函数或模板类成员可能存在问题却没有检查出来,因为可能该函数没有被具化使用到,直到具化到时才会检查,这便是模板参数类型在不同的类型下可能部分函数可用部分不可用的问题;故编译器实现者决定了其对模板代码的检测程度。
二、异常处理
- 编译器需支持异常捕获处理时,需找出catch子句以处理抛出来的异常。此时需要跟踪程序的堆栈,查询异常对象的方法和实际的类型(RTTI)以及管理抛出的对象,包括对象的产生、存储、析构清理或一般的存储等。编译器需要对此产生必要的数据结构并与异常库结合,基于程序大小和执行速度的考虑,进行选择折中(即考虑对应数据结构的构造时机,编译时加入可能影响程序大小,执行时加入可能影响程序执行效率)。
- 异常处理三元素:throw、try块、catch捕获,throw可以抛出内置类型或用户自定义类型的对象,try执行必要的语言块,并尝试验证测试内部可能出现异常,catch则捕获try块中的抛出的异常。
- 异常处理机制中,因为存在异常时,之后的代码无法被执行到,故需要特别处理好一些资源释放的问题,如内存、文件句柄等,可通过类包装的方式包裹资源释放操作(因在抛出异常之前的有析构函数的类对象一定会被调用到析构函数释放的(RAII机制))。此后异常被抛出依次释放局部变量对象,向上层继续抛出异常对象,函数出栈等,直到最终也没有被捕获时将调用terminate终止程序。
- 同样的,对含有析构函数的类对象数组的申请时若某个元素失败或者某个元素的类继承层次中抛了异常,则编译器需要保证对应元素部分内容的析构操作,以及所有已成功的元素的析构调用。
- 异常处理发生时的执行步骤:
1) 检验throw操作的函数;
2) 决定throw操作是否发生在try区段;
3) 若是,则把异常对象的类型与每个catch子句对比(依赖RTTI机制);
4) 若吻合时,则流程控制交给catch子句中(catch子句可以处理异常或其他的资源释放或恢复等操作,也可以继续抛出原异常对象(throw;即可)或抛出新的异常对象(throw newException(););另外catch子句若用基类以值的方式而不是引用来捕获子类异常对象,则会出现截断;
5) 若throw操作不在try区段或没有找到合适的catch子句,则系统需销毁当前的局部变量对象,然后从堆栈中将当前函数弹出掉,再进入到程序堆栈中的下一个函数中,重复以上步骤2~5。直到没有找到则终止程序。
6) C++编译器支持EH(异常捕获)机制会产生执行速度和程序大小的代价。
三、执行期类型识别(RTTI)
- 类型安全的向下转型,一个安全的向下转型需要在执行期可以对指针进行查询,确定可以正确且安全的向下转型。因为需要支持查询,则需要在转型时的空间和执行时间上的额外负担。额外负担有:
1) 额外空间以存储类型信息,通常为一个指向某类型信息节点的指针;
2) 额外时间以决定执行期的类型。
C++利用虚表,将类相关的RTTI对象地址(即type_info对象地址)放在虚表中(一般为第一个slot),此时编译器在编译的时候设定vtpr和虚表即可,当需要向下转型时利用RTTI决定执行期类型等信息。
2. 类型安全的动态转换dynamic_cast可以确保转化合适的类型对象,若失败则返回空指针(当转化为指针类型时)或者抛出异常(当转化为引用类型时)。dynamic_cast的成本在于运行期的类型识别判断。
3. 使用typeid运算符获取对象的类型,即type_info,当然也可以通过typeid来确定是否满足某个类型,然后调用static_cast转化即可。即if(typeid(someObj) == typeid(someType) ) {someType &rf = static_cast<someType&>(someObj);}。此外RTTI不仅可用于多态类,也可以用于内置类型和非多态的用户自定义类型。