文章目录
Template的“实例化”行为(Template Instantiation)
然而,member functions(至少对于那些未被使用过的)不应该被“实例化”。只有在member functions被使用的时候,C++ Standard才要求它们被“实例化”。目前的编译器并不精确遵循这项要求。之所以由使用者来主导“实例化”(instantiation)规则,有两个主要原因:
- 空间和时间效率的考虑。如果class中有100个member functions,但你的程序只针对某个类型使用其中两个,针对另一个类型使用其中五个,那么将其他 193个函数都“实例化”将会花费大量的时间和空间。
- 尚未实现的机能。并不是一个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由三个主要的语汇组件构成:
- 一个throw子句。它在程序某处发出一个exception。被抛出去的exception可以是内建类型,也可以是使用者自定类型。
- 一个或多个catch子句。每一个catch子句都是一个exception handler。它用来表示说,这个子句准备处理某种类型的exception,并且在封闭的大括号区段中提供实际的处理程序
- 一个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发生时,编译系统必须完成以下事情:
- 检验发生throw操作的函数。
- 决定throw操作是否发生在try区段中。
- 若是,编译系统必须把exception type拿来和每一个catch子句进行比较。
- 如果比较后吻合,流程控制应该交到catch子句手中。
- 如果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)
- downcast(向下转换)
意为 将基类转换至其派生类中的某一个。
需要downcast的情况:指向派生类对象的基类指针,需要使用派生类的非虚函数,此时需要先把基类指针转换为派生类指针,再调用函数。
downcast引发的问题:如果downcast不正确的使用,比如:被转换的指针并不是指向该派生类对象,而是指向其他派生类对象或基类对象,则也会被转换且不会报错,后续使用会造成问题。
- 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。
- 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运算符。