<引言> 函数模板是C++中的高级概念,初级用户即使使用了一些标准库中的函数模板都未必意识到。 作为强类型语言的C++,很多时候往往制约了一些较强语言功能的发挥,比如在Perl/PHP等脚本语言中,类型的模糊为实现普遍的算法 (比如min/max)提供了较好支持,同样的算法可以施加在各种不同数据类型上,而讲这些差别交给解释器完成,无疑给用户提供了很大的 方便。 在C中一般只能通过使用宏达到类似的效果,但显然这不是个类型安全和稳妥的方法。而C++就必须为各种类型编写函数(比如用 重载函数),更好的方法便是使用函数模板.
<定义> template <temp_par1,temp_par2,...> temp_par是模板类型参数(template type parameter).它代表了一种类型,也可以是模板非类型参数(template nontype parameter), 它代表一个常量表达式. template type parameter由class或typename后加一个标识符构成,在函数的模板参数表中这两个关键字的意义相同它们表示后面的 参数名代表一个潜在的内置或用户定义的类型,模板参数名由程序员选择譬如 template <class Glorp> Glorp min( Glorp a, Glorp b ) { return a < b ? a : b; } template nontype parameter则在模板实例化期间是常量 如同非模板函数一样函数模板也可以被声明为inline 或extern 应该把指示符放在模板参数表后面而不是在关键字template 前面,如: template <class Glorp> inline Glorp min( Glorp a, Glorp b ) { return a < b ? a : b; }
<关于typename和class> 为了分析模板定义,编译器必须能够区分出碰到的表达式是一个计算表达式还是声明类型的表达式.对于编译器来说它并不是件容易的事情。 例如如果编译器在模板定义中遇到表达式Parm::name, 且Parm 这个模板类型参数代表了一个类,那么name 引用的一定是 Parm 的一个类型成员吗 template <class Parm, class U> Parm minus( Parm* array, U value ) { Parm::name * p; // 这是一个指针声明还是乘法乘法 } 显然,编译器不知道name 是否为一个类型,因为它只有在模板被实例化之后才能找到Parm 表示的类的定义.为了让编译器能够分析模板 定义,用户必须指示编译器哪些表达式是类型表达式.告诉编译器一个表达式是类型表达式的机制是在表达式前加上关键字typename. 例如:如果我们想让函数模板minus()的表达式Parm::name 是个类型名,因而使整个表达式是一个指针声明我们应如下修改 template <class Parm, class U> Parm minus( Parm* array, U value ) { typename Parm::name * p; // ok: 指针声明 } 这其实就是C++的关键字typename之所以出现的最初原因,现在则主要用在模板参数表中以指示一个模板参数是一个类型.
<实例化> 函数模板在它被调用或取其地址时被实例化
<模板实参推演 template arguments deducation> 在模板实参推演期间决定模板实参的类型时,编译器不考虑函数模板实例的返回类型 模板实参推演的通用算法如下 1 依次检查每个函数实参,以确定在每个函数参数的类型中出现的模板参数 2 如果找到模板参数,则通过检查函数实参的类型推演出相应的模板实参 3 函数参数类型和函数实参类型不必完全匹配.下列类型转换可以被应用在函数实参上,以便将其转换成相应的函数参数的类型 A. 左值转换(数组到指针,函数到指针) B. 限定修饰转换(const,volatile) C. 从派生类到基类类型的转换. 4 如果在多个函数参数中找到同一个模板参数,则从每个相应函数实参推演出的模板实参必须相同 从这里可以看出,模板实参的推演并不允许有序标准转换,因为这是在确定实例化哪个函数,而不是确定调用哪个函数
<显式模板实参(explicitly specify)> 比如: sum<char,unsigned int,int>(a,b,c) 此时,就没有必要推演模板实参了,函数参数的类型已经固定。当函数模板实参被显式指定时,把函数实参转换成相应函数参数 的类型可以应用任何隐式类型转换(当然包括有序标准转换) 显式模板实参应该只被用在完全需要它们来解决二义性,或在模板实参不能被推演出来的上下文中时
<模板编译模式> 包括Inclusion Model和Separation Model, 对于前者,每个模板被实例化的文件中包含模板定义,并且定义通常放在头文件中,就和inline函数的定义一样。缺点是, 没有把实现和声明分离开,并且可能导致同样的代码被多次编译,同时造成多次实例化(这可以通过显示实例化来解决). 对于后者,模板函数实现放在单独的实现文件中,头文件只是模板函数的声明。但模板函数实现的定义需要出现export关键字。 这样的好处是分开了接口和实现,缺点是需要仔细规划代码,尽量防止在多个文件中出现export同一个函数模板,这有可能造成 链接错误,另外一个遗憾是,目前很少有编译器支持Separation Model(也许gcc或者VC7已经支持了)
<显式实例化声明> 为了是模板更加实用,标准C++提供这个机制帮助用户自行确定函数实例化的时机,用法如下: template <typename Type> Type sum( Type op1, int op2 ) { /* ... */ } // 显式实例化声明 template int* sum< int* >( int*, int ); 则int*的函数被显式实例化.对于给定的函数模板实例,显式实例化声明在一个程序中只能出现一次,在显式实例化声明所在 的文件中函数模板的定义必须被给出.如果该定义不可见则该显式实例化声明是错误的. 显式实例化声明是与另外一个编译选项联合使用的.该选项压制了程序中模板的隐式实例化. 选项的名称随着编译器不同而不同. (感觉这个机制并不是很有用,不知道gcc的选项是什么)
<模板显式特化(explicit specialization definition)> 虽然模板已经为我们做了很多有益的工作,但有的时候,我们不希望这个模板函数实例化我们所有的类型,通常是因为有些类型 如果例外处理反而对性能和功能有更大的好处。 比如说,对一个比较相等函数来说,对于普通类型(int,bool),它们的操作基 本上是bitwise的比较,而对于C风格的字符串类型(char*),我们需要的则是memberwise的比较。因此C++提供模板显示特化 机制,给我们一个“重载”的机会。 例如: // 通用的模板定义 template <class T> T max( T t1, T t2 ) { return (t1 > t2 ? t1 : t2); } // const char* 显式特化: // 覆盖了来自通用模板定义的实例 typedef const char *PCC; template<> PCC max< PCC >( PCC s1, PCC s2 ) { return ( strcmp( s1, s2 ) > 0 ? s1 : s2 ); } 注意: 在源文件中使用函数模板显式特化之前必须先进行声明 通常, 模板显式特化的声明被包含在每个用需要被特化的类型实参调用函数模板的文件中. 显式特化的声明应该被放在头文件中, 并在所有使用函数模板的程序中包含这个文件
<重载和函数模板> 函数模板同样可以被重载,只是名字解析由于模板实参推演过程的加入而变得稍许复杂了。主要的步骤如下: 1. 编译器看到一个函数调用点时,先确定所有同名的调用点的函数。除了普通的重载函数,候选函数集中还要再加上那些 实参推演成功的模板函数,此时如果存在模板特化,则实际上该模板特化成为一个候选函数。 2. 此时确定可选函数集(根据3种标准转换),这一步和普通的重载解析一致。 3. 对各种转换进行分级打分,最后剩下的函数中,如果同时有普通函数和模板函数入选,则只取普通函数,如果普通函数不止 一个,则编译报错。显然,如果最后剩下的函数中没有模板函数,那么结果和一般的重载函数解析一致,即如果不止一个函数满 足要求,编译报错。 这里有一个问题可能会引起困惑。为什么同时有普通函数和模板函数入选时要优先考虑普通函数。 这是为了应付这种情况,当 我们不得不对某种类型采取显式模板实参调用模板函数时,为了不修改整个文件中的多处函数调用,就直接用一个普通函数来声明, 而在普通函数里一次性完成显示模板实参调用。如以下所示: // 函数模板定义 template <class Type> Type min( Type t1, Type t2 ) { ... } // 普通函数 int min( int a1, int a2 ) { min<int>( a1, a2 ); } int main() { // 调用普通函数 min( ai[0], ss ); } 设想,如果在源程序很多地方都需要调用min<int>(int,int),这不失为一个比较好的利用语言规则的措施,因为即使不利用这样的 规则,对程序员来说使用流行的编辑器软件完全替换所有函数调用点也绝对是不难的事情。
<模板定义中的名字解析> 在模板定义中,有些名字符号是需要到模板实例化时才能解析的,它们一般是模板的实参类型。而有些则是在模板定义时就可以 解析的。前者通常称为依赖于模板参数的名字(depend on a template parameter),而后者显然是不依赖于模板参数的。 对于不依赖于模板参数的函数调用,必须在定义模板前事先声明。 而对于依赖模板参数的函数调用,则必须在模板实例化前声明。 因此,显而易见的推论是,不依赖模板参数的名字都由模板库的提供者声明,而用户只在需要实例化时给出名字的声明即可。
<名字空间和函数模板> 实际上很简单,把函数模板定义在一个名字空间里,就和把一个普通函数定义在名字空间里一样,使用前,或者using一下需要的函数, 或者把整个名字空间一起using进来。 这两者实际上没有本质的关联。
<ChangLog>
2004.8.25 修正笔误,改变某些段落使之更流畅,便于理解。
2004.8.12 Initial version released