第七章:模板
一、模板的实例化
1.模板类中定义的静态成员变量和枚举类型必须等到该模板实例化之后才可以使用,并且必须显式的指定类型。
template <class Type> class Point { public: enum Status{unallocated,normalized}; private: static int chunkSize; }; Point::Status s; //错误 Point<float>::Status s; //正确,如果没有Point<float>实例,则实例化一个
2.指向一个模板类型的指针不会导致实例化,但是引用必定导致实例化
Point<float> *ptr=0; //并不会实例化 const Point<float> &ref=0; //实例化Point<float>并且必须可以转换为整数0
3.未使用的函数无需实例化。其原因主要有两点:
①空间和时间的效率。如果类中有很多函数,用户只使用其中几个,全部实例化会浪费大量时间。
②尚未实现的技能。并不是每一个类型的实例化都能用于所以成员函数,如果全部实例化可能会导致编译错误。比如int实例化支持+操作,bool实例化则不支持+操作。
4.只有语义错误(lexing error)被要求在编译器处理模板声明的时候被标识出来。有的编译器会在此时也进行解析parse,但是不做类型检查(cfront编译器),有的则是将模板的声明收集为一系列的lexical token(这时候做语义检查),当一个实例化发生时,这组token被推往解析器parser,然后调用类型检查。
二、模板的名称解析(Name Resolution)
1.定义模板的程序段“scope of the template definition”和实例化模板的程序段“scope of the template instantiation”
前者指定义函数模板的那个代码段,后者指模板实例化的代码段。
//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; //有两个函数①定义模板时的double foo(double)②实例化时的int foo(int)
2.模板之中对于非成员名的解析是根据这个名字是否与“用以实例化该模板的参数类型”有关而决定的。如果与其使用互不相关则以定义模板的程序段来决定名字,如果与其使用相关则以实例化模板的代码段来决定(因为定义模板时还没用到具体类型)。并且解析的结果只和函数的签名有关(函数名,参数个数,参数类型)和返回值无关。
sr0.invariant(); //与类型无关调用double foo(double) sr0.type_dependent(); //与类型有关调用,int foo(int) ScopeRules<double> sr1; sr1.type_dependent(); //与类型有关调用double foo(double);
三、模板的实例化行为
编译器设计者必须回答的三个主要问题
1.编译器如何找出函数的定义
答案之一是包含模板程序文本文件,他像头文件一样,另一个要求是文件命名规则,如Point.h文件的函数声明其模板程序文本必须放在Point.C或.cpp中。
2.编译器如何能够只实例化程序中用到的成员函数
一种方法是忽略这项要求,全部实例化。另一种策略是模拟链接操作,看看真正需要个函数,然后只为他们产生实例。
3.编译器如何阻止成员定义在多个.o文件中都被实例化(没有链接之前,在一个文件一个实例化就要产生一个定义)
解决办法之一就是产生多个实例,然后从链接器中提供支持,只留下其中一个实例。另一个办法就是使用者来引导模拟链接阶段的实例化策略,决定留下哪些实例。
然而上述三个问题的处理都存在当模板实例被产生时,有时会增加大量编译时间的问题或是决定哪些实例是被需要的时候所话代价太大。C++模板的意图是一个由使用者引导的自动化实例机制,但是这是非常难以完成的任务,现在几乎都存在缺陷。
4.Edison Design Group开发的第二代directed-instantiation机制很接近自动化实例机制:
①一个程序的原始码被编译时,并不会产生实例化,但是先关信息已经产生于.o文件(目标文件)中
②当.o文件被链接在一起时,会有一个prelinker程序被执行。他检查.o文件,寻找模板实力的相互参考以及对应的定义。
③对于每一个"参考到木板的实例”而没实例却没有定义的情况,prelinker将改文件视为与另一个实例化等同(在其中实例已经实例化)。以这种方法,就可以将必要的程序实例化操作指定给特定的文件。这些都会注册在prelinker所产生的的.ii文件中,存放在磁盘目录ii_file。
④preliker重新执行编译器,重新编译每一个.ii文件曾被改变过的文件。这个过程不断重复直到所有必要的实例化操作都已完成。其中重新编译包括,对于每一个将被重新编译的程序文本文件,编译器检查其对应的.ii文件,如果对应的.ii文件列出一组要被实例化的模板,那些模板将在此次编译时被实例化,prelinker必须被执行起来,确保所有被引参考到的模板已经被定义好。
⑤所有的.o文件都被链接成一个exe文件
所以上述方案的成本主要是第一次编译时.ii文件的设置时间,次要成本是对于每一个需要被编译的模板执行prelinker,以确保所有被参考到的模板存在定义。
5.包含虚函数(虚基类)的模板
第四点提到的方法没法确保包涵虚函数的模板中只有一个虚表实例被产生。
C++标准要求如果一个虚函数被实例化,其实例化点紧跟在其类的实例化点之后。因为包含虚函数的类在实例化后必定包含虚函数表,而虚函数表必定存放着虚函数的地址,虚函数的地址存在代表着虚函数已经被实例化,所以虚函数表产生时,所有虚函数都必须实例化且实例化点跟在类的实例化点之后。
第四点提到的自动实例化机制并没有考虑到这点,在Point的float实例有一个虚析构函数被实例化之前,虚表不会产生,而且这个虚析构函数是隐式调用的,所以他没有他放进.ii文件中,导致链接期无法找到虚函数表。
之前C++标准要求虚函数表放在第一个定义了该类的非虚、非纯虚函数的文件中,但是许多编译器还是产生多个虚表,只是忽略了多余的,现在C++标准以及放松了只产生一个虚表的这种要求。
6.为了应对第五条因为需要隐式调用虚函数,而不能自动创建虚表的情况,C++允许程序员在文件中将整个类模板显式实例化或者针对一个类的模板函数实例化。

浙公网安备 33010602011771号