导图社区 Cplusplus函数模板(下)
这是一篇关于Cplusplus函数模板(下)的思维导图,C 函数模板详细讲解下:函数的标识符解析,STL模板编程示例
编辑于2021-08-13 22:33:53C++函数模板(下)
函数模板的编译
包含式
在包含编译方式下,模板在头文件中定义。如果某个源文件需要实例化模板,就包含模板所在的头文件
虽然定义模板的头文件可以被包含在许多源程序文件中,但是这并不意味着每次调用前都要进行实例化。对于某种类型,编译器只实例化模板一次。但是,真正的实例化动作发生在何时何地,要取决于编译器的具体实现。
分离式
在分离编译方式下,头文件中是函数模板的声明,而模板定义则放在另外的源文件中
在定义模板的源文件中,在关键字template前加上关键字export,表示声明一个可导出的函数模板。
虽然分离方式能够很好地分离模板的声明和定义,但是并不是所有的编译器都支持这种编译方式,即使支持也未必总能支持得很好。所以,在定义模板时最好总是放在头文件中,否则程序的可移植性就难以保证。
函数模板实例的编译时机
函数模板本身并不能直接使用,必须实例化后才能使用。但是由于数据类型众多,不能为每一种数据类型都实例化一个模板实例,所以编译器采取的策略是直到使用时才编译相应的实例。
当遇到该模板定义时,编译器只是保存了该模板的内部形式,而不直接进行编译,直到在程序中遇到类似右侧的使用才编译:
模板的某个实例可能会被多次使用,而且是在不同的地方(可能在不同的函数中)。而且由于程序的运行是动态的,编译器无法确知模板的某个实例何时被第一次使用。对于早期的C++编译器,这是一个难以解决的问题,所以通常的策略是编译每一个函数模板实例。当然这么做也有问题,会导致模板的同一实例被编译多次。尽管编译器会从多个编译结果中选择一个作为最终结果,但是编译的时间有可能会大大延长。
为此,C++标准允许使用显式实例化声明,用来确定模板实例化的时间,从而解决一个模板实例被多次编译的问题。
模板Add的int型实例的显式声明
尽管 C++标准没有规定模板实例显式声明的位置,但是模板某个类型实例的显式声明在程序中只能出现一次。如果出现两次或两次以上,则会导致编译错误。另外,在显式声明出现之前,模板的定义必须是可见的,否则也会导致编译错误。
模板实例的显式声明只对一个源文件有效。如果在其他源文件中也使用到了该实例,那么为了消除多余的模板实例,就需要借助一个编译选项,该选项在不同的C++编译器中有不同的名字。例如在IBM编译器Visual Age for C++ for indows 3.5版本中,该选项为/ft-,在其他编译器中的名字请参看相应的编译器手册。
函数模板的标志符解析
定义
编译器在处理函数模板的定义时,一个很重要的任务就是要找到被调用的函数或者使用哪个函数模板实例,这就是所谓的标识符解析。
示例
模板Foo的定义中两次调用了函数printData。不过,第一次调用的参数是整形数,第二次调用的参数是模板Foo的函数形参t1,而t1的类型未知,依赖于Foo的模板实参。
由于第一次调用所引用的printData重载版本是确定的,所以其相关定义必须出现在模板Foo的定义之前。
第二次调用所引用的printData重载版本并不确定,依赖于模板实参,所以相关定义并不要求出现在Foo之前,但是,这个相关定义必须出现在Foo的相关实例之前。
例如,在主函数中调用Foo( 3.14 ),这意味着Foo的实例中要调用printData(3.14),所以printData的double型重载版本必须在此之前定义。
总结
C++标准规定,编译器对模板定义中的标识符解析应当分为两个步骤:
首先,不依赖于模板参数的标识符在模板定义时被解析
其次,依赖于模板参数的标识符在模板实例化时被解析。
重点
函数模板的特化
起源
很多时候,定义一个适合所有类型的函数模板非常困难,这主要是因为对于某种逻辑操作,各种数据类型的实现并不相同。
同样是比较大小,整型和浮点型数的比较就与字符串类型的比较不同。整型和浮点型数据只要调用各种比较运算符(<,>,==等)即可,但是字符串的比较就需要调用专门的比较函数,例如strcmp和wstrcmp。而自定义数据类型,如结构体、类,比较大小就更加特殊。在这种情况下就需要用到函数模板的特化。
举例
const char*型实参实例化max模板,程序员的本意是比较字符串的大小,但实例化的结果却是比较两个指针的大小。
为了获得正确的语义,必须针对const char*类型,为函数模板max提供特化的版本。
模板特化定义
由于有了这个特化版本,当在程序中调用函数max(const char*,const char*)时,真正被调用的是特化的版本,而不是用类型const char*实例化的模板。对所有用两个const char*型实参进行调用的max都会调用这个特化的定义,而对于其他的调用则都是先实例化模板,然后再调用。
总结
对于模板实参列表,如果函数模板的返回类型与函数参数的类型相同,则该列表也可以省略。例如在上例中,参数的类型和返回类型都是PCC,所以上述特化模板也可以写成template<> PCC max( PCC s1, PCC s2 ) {……}。
函数模板的重载
和普通函数一样,函数模板也可以重载。重载的函数模板,模板名称相同,但函数形参列表不同。
两个模板的模板形参相同,都是一个类型(typename Type)
模板名称也相同,都是min。
但函数形参列表不一样,一个是( const Type *, int ),另一个是(Type, Type)。
同函数重载一样,函数模板的返回类型并不是重载的判断条件。因为在某些情况下,调用函数模板并不需要考虑返回值。
示例
在上述主函数的定义中,第6行的第二个参数是一个整型数1024。从定义上来讲,此处更适合第一个函数模板,所以,该处的调用得到的模板实例就是第一个模板的实例。
总结
如果存在多个候选的模板,那么在实参推演过程中,编译器倾向于选择那些函数实参跟形参类型相近的模板。例如在上面的例子中,实参1024是一个整型,而候选函数模板中,其中一个的形参也是整型,那么该模板就会被选来进行实例化。
重载函数没有二义性的问题,而重载函数模板则可能导致二义性。
两个参数类型相同的模板
两个参数类型不同的模板
这里的实例min就会导致二义性
因为在这个例子中,编译器无法决定到底该实例化哪个模板。第二个函数模板,虽然其模板参数声明为两个类型(T和U),但并没有限制这两个类型必须不同
所以语句“min( 1024, 512 );”也可以看做是调用第二个模板的实例。显然这样做会引起与第一个模板的冲突,从而导致编译错误。
但是,在这种情况下其实可以不必重载函数模板。因为所有能够匹配min( T, T )的调用,也完全可以匹配min5( T, U )。所以应该只提供min( T, U )的声明,而min( T, T )应该被删除。
在某些情况下,尽管可以采用重载函数模板,但在进行程序设计时,仍然应当尽量少地使用,以避免不小心带来的二义性。
函数匹配规则
函数模板可以与普通函数同名。例如在一个程序中可以同时具有一个 min 函数,也可以有一个或多个min函数模板。在这种情况下编译器需要决定是用非模板函数,还是函数模板实例。
C++标准规定:非模板函数具有最高的优先权,如果不存在匹配的非模板函数,则最匹配的和最特化的函数具有高优先权。
如果在多个重载的函数模板中,有一个模板在进行模板实参推演时不需要类型转换,则该模板就是最匹配的。如果对于调用时的实参类型,存在特化的版本,并且类型也不需要类型转换,则该模板称为最特化的。
示例
综合实例
给出几个使用函数模板的完整实例,来说明如何定义和使用函数模板。标准C++的标准模板库(STL)是一个比较完备的模板库,读者在学习模板的时候可以参考该库的实现。
数组求和模板
数组可以支持的数据类型多种多样,如整型、浮点型、字符串等。本实例要求定义一个函数模板,其参数是数组名和元素个数,返回值是数组中各元素的和。由于字符串比较特殊,所以应当专门针对字符串数组进行特化。
对于没初始化的元素会自动初始化为'\0'(关于C之空字符与空指针)(注:如果完全没有初始化任何一个元素,则不是0而是垃圾数据)。
总结
使用模板可以将处理数据的算法进行泛化。泛化的好处是可以将一种算法应用到多种不同的数据上,从而避免相同逻辑的无意义重复,减少代码量。但是对于某些数据类型应用某个函数模板可能并不合适,此时就可以针对该数据类型对函数模板进行特化处理。
在程序中使用函数模板,实际上是使用该模板的实例。在程序编译过程中,编译器根据实参类型推演出具体的模板参数,然后用这些模板参数实例化模板,并将产生的实例编译成具体的机器码。模板实参推演并不要求参数类型完全匹配,如果在实参和形参之间存在左值转换、限定修饰符转换或到基类的转换,那么编译器也可以推演出正确的数据类型。
模板的编译有两种方式:一种是包含式,即模板的定义在头文件中;另一种是分离式,即模板的声明和定义分别放在头文件和源文件中。目前大多数编译器只支持前者,即包含式,但包含式有可能导致重复实例化。此时可以使用模板显式实例化声明,并结合抑制隐式实例化的编译选项,来强制实例化模板。函数模板也可以重载,即定义名称相同但函数形参列表不同的模板。注意,在重载函数模板的实例化过程中,有可能导致二义性的问题。