函数模板
-
c++对于泛型编程和元编程的具体实现方式,某些情况下对函数重载和宏定义的上位替代,也用于同种功能的函数处理不同数据类型的的代码重用需求,使用重载函数虽然使用时直观,但实际定义时的代码量并未减少,而使用宏定义虽然达到了一个抽象处理不同类型实例的目的,但编译器对宏定义替换不作检查,同时宏定义的传入参数不可有副作用,易出错,而函数模板和模板函数则很好的规避了上述问题
-
定义格式:
template <class NAME1,...> return_type function_name (NAME1 argument1,...) {...}
或
template <typename NAME1,...> return_type function_name (NAME1 argument1,...) {...}
(即在模板中使用的class和typename作用完全相同)
-
模板函数:由模板实例化的具体实现的函数成为模板函数,即对于模板的定义在编译器看来属于一种特定的声明,及编译器并不会为定义的模板本身分配内存空间,只有当具体使用模板生成了处理特定类型的模板函数之后才会分别为每个模板函数分配内存空间,其模板函数的生成方式分为显式生成和隐式生成:
-
显式实例化:通过显式声明告诉编译器根据模板生成特定的模板函数,如(针对2.中模板实例化):
template return_type function_name<int, ...>(int a,...);
或
template return_type function_name<>(int a, ...);
或
template return_type function_name(int a,...);
-
隐式实例化:直接传入实参调用函数调用,编译器根据传入的参数自动实例化特定模板函数,如:
function_name\<int\>(a,...); //将生成fucntion_name<int> function_name<>((int)a,...); //将生成function_name<int, int> function_name((char)a,...); //将生成function_name<char, char>
-
注:一般在使用的时候主要采用隐式实例化函数模板,很少采用显式实例化
- 使用模板定义的函数本身不会被实例化,即不会为其分配空间,只有在实际调用时,模板函数根据传入的参数类型明确了特定类型,编译器才会为其分配空间将其实例化
- 与宏定义不同,同一模板在模板函数同一次调用时只能被替换为一种特定类型,故形参全部定义为同一模板的模板函数(
template \<class A\> A function(A a,A b){...}
)在实际调用时传入的形参类型必须相同,若传入参数不同(如:function((int)234,(char)'a');
),则模板将无法判断到底将 A 替换为何种类型,解决方式有两种:- 一是利用函数重载,写一个该函数的特定类型重载
int function(int a,int b){...}
,则在实际调用上述不同参数时,会选择特定函数并通过数据类型之间的隐式转换功能将char替换为int 处理(模板函数没有隐式转换功能),同时对于重载的一般特定函数和模板函数,实际调用时编译器总是尽可能优先考虑一般的特定函数进行调用 - 二是非必要情况下不要将模板函数的形参定义为同一模板类型,而是定义为不同的模板类型(
template <class A , class B>A function(A a,B b)
注意这里返回值随第一个传入形参的类型)
- 一是利用函数重载,写一个该函数的特定类型重载
- 模板函数实际是将具有相同功能的函数集合抽象出来,作为一个公有模板,在实际调用时对其进行特定类型的实例化
- 同理,模板函数可以对任何特定类型进行替换,故也可以对传入的自定义类型进行自动替换,但需要注意的是,当处理对象为自定义类型时,函数体中的所有操作必须都存在默认或自定义该类型的重载(如:类类型的加法运算符 + 重载,比较运算符重载 < 、 >),否则虽然模板可以正常替换但会因为具体实现中编译器无法识别某运算符作用在该自定义类型上的操作而发生错误
- 模板的定义只能是全局,不能在局部定义模板
- 编写函数模板的通用步骤:
- 先编写处理特定类型的一般函数
- 调用并测试该函数
- 将测试通过的函数改为泛型数据类型的模板
注:即在不熟悉的情况下,不要直接编写函数模板,而是先编写特定类型的一般函数,再改为泛型函数模板
-
函数模板的特化:如果将函数模板视为对各种数据类型的集合给出通用处理过程的方式,那么函数模板也支持对这个所有数据类型的集合中的某个特定的数据类型给出不同于通用处理方式的特定处理方式,即称之为函数模板的特化,参考下例:
- 当定义了一个实现两个任意类型的数相加的函数模板时
template<typename A, typename B> auto add(A x, B y){ return (x + y); }
似乎看上起很完美,可以将A,B视为所有类型的集合表示,对于任何传入参数的类型都可以给出相同的处理(如:int double, int int, int float, string string),但值得注意的是,对于某些特定类型之间是无法进行加法运算的(如:int string),当错误的传入这类参数组合时,模板虽然依旧会对应给出实例化,但在函数体中具体执行时就会报错(执行了:return (int + string);),那么对于这类特定的输入组合,除了人为确保不会给出错误输入外,还可以针对这类输入指定特定的处理方法,即函数模板的特化,如:
template<> //函数模板的特化的固定格式 auto add<int, std::string>(int a, string b) { //<>及其中内容可以省略 return "error: these data cannot be added"; }
如上,
template<>
是特化的固定写法,在下方给出函数头部分,函数名后通过<>给出想要特化的参数(可省略,编译器可以通过后面的形参列表中的参数类型进行推导),后面的形参列表中的模板参数也替换为具体的参数,然后给出函数体(特化的实现)特化后,当再次错误地传入int string参数试图相加时,就不会再对模板本身给出该组参数的实例,而是直接执行特化给出的特定处理过程了
注:函数模板的特化本身实际上就是一个特殊的函数,编译器会在定义时就为其分配空间
-
函数模板的重载:如同函数重载,函数模板也可以进行重载,其具体意义如同类模板的偏特化,如果说函数模板的特化是针对某一特定类型给出特定的处理方式,那么函数模板的重载则是针对某些具有相同特征的数据类型给出特定的处理方式,参考下列例子:
- 当定义了一个实现两个任意类型的数相加的函数模板时:
template<typename A, typename B> auto add(A x, B y){ return (x + y); }
已经知道,对于某一两个特定的类型不能采用通用的处理方法时可以进行特化(如:int string),但是当具有某一相同特征所有数据类型的集合都不能采用通用的处理方法时则很难用特化解决(如:对于上例,当输入的两个数据类型都是指针,无论是何种类型的指针,都没办法直接返回二者相加的值(因为是地址相加),同时也不可能将所有类型的指针都特化一遍)在类模板中解决的办法是偏特化,而函数模板则不支持偏特化,但可以考虑用模板重载实现,如:
template<typename A, typename B> auto add(A* x, B* y){ return (*x + *y); }
当进行如上重载后,再调用该函数而传入的是指针变量的时候,虽然两个重载都能够满足模板参数替换要求(如传入int* double*,第一个参数:A–int*,B–double*; 第二个参数:A–int,B–double)但编译器会选择更为具体的模板(所能处理的数据类型更少)进行实例化,即下面的重载,也就解决了对于所有指针的处理的问题
注:函数模板的重载之间的关系,满足在所能处理的数据类型集合上为包含与被包含,在实际调用中编译器会选择更为具体的那个模板进行实例化,如上,第一个可以支持处理所有的数据类型,而第二个则只能支持所有的指针类型(这里的支持为写法上的支持,不考虑具体执行),故第二个重载的所能处理数据类型的集合包含于第一个,函数模板之间的重载也必须满足该关系才能实现(即两个重载所能处理的数据类型的集合不能相等)