函数模板和类模板是泛型编程的基础,简要来说,函数模板可以用来表示一类函数,这类函数的基本流程,或者说语义、作用基本一致,区别在于其中的参数会导致实现的略有不同,我们把这种参数化的函数叫函数模板。
1.函数目标一瞥
函数模板与普通函数的行为看起来很相似,只是函数模板代表了一类函数,与普通函数相比,函数模板在定义的时候模板参数是未知的(模板参数的说法与普通函数的参数列表相对,是不同的概念)。下面是一个例子。
template<typename T>inline T const&max(T const &a,T const &b){return a>b?a:b;
}
函数模板必须有模板标识,如上面例子中的template<typename T>,模板标识的语法是template<comma-separated-list-of-parameters>,即由逗号分割开来的一些模板参数,typename 是模板参数的标识,由于历史原因,最早的模板标识是class,沿用至今,也是可以的,但是建议用typename,这样不会让人误解,因为模板参数可以为class type,也可以不是,但是,struct是不可以的。
3.使用模板
//basics/max.cpp
#include <iostream>#include "max.hpp"
#include string
int main()
{int i=42;
std::cout<<"max(7,i): "<<::max(7,i)<<std::endl;
double f1=3.4;
double f2=-6.7;
std::cout<<"max(f1,f2): "<<::max(f1,f2)<<std::endl;
std::string s1="mathematics";std::string s2="math";std::cout<<"max(s1,s2):"<<::max(s1,s2)<<std::endl;
}
在这个程序中,max调用了三次,全局符号::是为了防止重名函数的歧义,比如说std命名空间下就有个max(),需要注意的是,对于这三次调用,编译器生成了三个不同的函数,比如说,对于max(7,i),调用的是T=int的这个函数模板,语义上调用的是如下的code:
inline int const&max(int const&a,int const&b){return a>b?a:b;
}把模板参数换成具体类型的过程称为具现化(姑且用侯捷先生的翻译,虽然和类的实例化容易混淆),产生的结果是模板函数的一个实例,需要注意的是,T在具现化的时候类型是open的,随意指定,但是如果在模板函数的执行逻辑里面涉及到一些指定类型处理不了的操作,甚至是没有定义的操作,结果将是编译错误,例如,对于复数,由于没有大小比较的函数,如果把T换成complex,那么就是编译错误。
因此,我们可以总结得到:模板需要编译两次:
第一次:没有具现化之前,模板代码检查语法错误。
第二次:具现化之后,检查所以代码是不是合法的,当然,这在具体实现的时候会有一些问题,当函数模板用触发的模式进行具现化的时候,也就说在运行的时候确定调用哪个模板的时候,会打乱正常的编译连接流程,所以,为了防止这个问题对你造成困扰,我们采用最简单的策略,用内联函数,避免了这一问题。
4.参数推导
参数推导,英文翻译argument deduction。如果看过stl迭代器实现那一章的朋友们可能会印象深刻,C++并不支持类型推导,并没有typeof之类的函数,即便动用RTTI性质中的typeid(),获得的也只是类型名称,不能拿来作为变量声明之用。
参数推导是这样描述的:当我们以某个类型参数调用一个函数模板,比如说max(),模板参数决定于我们传过去的参数,这句话听起来好无聊,但是后面的一句话不无聊,C++编译器保证这里面没有任何类型转换,是int 就是int ,不能转换成double等,必须严格匹配,比如:
template <typename T>inline T const &max(T const&a,T T const &b);...max(4,7);//正确
max(3,4.2); //编译错误,第一个T是int,第二个T是double
要处理这类错误,有以下几种解决方案:
- 调用前的显式类型转化:max(static_cast<double>(4),4.2)
- 显式指定模板型别参数:max<double>(4,4,2)
- 重新定义模板:template<typename T1,template T2)
型别参数可以在函数体里面声明变量,请记住,这是个很有用的功能。
5.模板参数详解
实际上,模板参数有两种:
- 类型参数,用typename界定的参数,
- 调用参数,对应于普通函数的参数列表
想要多少模板参数就可以有多少,但是,模板参数不能够有默认值,至少目前为止是这样的,这点与类模板不一样。再给一个例子,
template <typename T1,typename T2>inline T1 max(T1 const &a,T2 const &b){return a>b?a:b;
}这个例子与之前的例子不同之处在于有两个类型参数,好像很好滴解决了max参数类型不同但是可以比较的问题,但是这种方式有很多不足:
- 返回值的类型必须指定,那么当实际返回值的类型与定义的不同时,会有强制类型转化发生
- 返回值的类型是值类型,如果返回值是一个类类型,class type,这意味着生成一个临时对象的较大开销,在这个例子里面貌似可以将返回值设为T1&,但是有时候这并不可行,比如返回值是一个局部变量的情况就不能返回引用。
对于第一个不足,可以额外定义一个返回值类型参数,然后加上之前我们提到的可以显式指定模板类型参数,就有了下面的这个使用例子:
template<typename T1,typename T2,typename RT>inline RT max(T1 const &a,T2 const &b);...max<int,double,double>(4,4.2);当然,上面的max<int,double,double>这种调用方法会让人很烦躁,实际上我们就是想指定RT的类型而已,对于这种情况实际上我们这样调用也是没问题的:
max<double>(4,4.2);
这其实同调用参数的默认列表使用有异曲同工之妙,如果要特殊指定某些类型参数,那么从第一个指定的类型参数开始,其之后的都应指定,于是,如果只给指定一个double,或默认认为指定的是最后一个参数。
6.函数模板的重载
与普通函数一样,函数模板也是可以重载的,普通函数的重载本身就是个比较复杂的问题,如果不熟悉,可以网上查查,一看一大堆,这里只讨论与模板相关的问题。先上一个例子:
//basics/max2.cpp
//notemplate function
inline int const &max(int const &a,int const &b){return a>b?a:b;
}template<typename T>inline T const & max(T const&a,T const &b){return a>b?a:b;
}template<typename T>inline T const& max(T const& a, T const& b){return max(max(a,b),c);
}int main()
{::max(7,42,68);//调用三个调用参数的模板函数
::max('a','b');//由参数推导,调用类型参数为char的模板函数
::max(7,42);//优先调用非模板函数
::max<>(7,42);//强制调用模板函数,有参数推导
::max<double>(7,42);//强制调用类型参数为double的模板函数,无参数推导}
上面涉及到的知识点有:
- 同样匹配的情况下,优先调用非模板函数
- 可以强制调用模板函数
- 强制的时候加了类型约束就没有参数推导了
- 可以看到,三调用参数模板函数中用到了二调用参数函数模板,而且返回值都是reference,这是个好习惯
7.总结
- 模板函数定义了一类函数
- 当你传递给模板函数一组参数时,模板函数会依据这些参数具现化一个函数
- 可以在调用的时候显示指定模板类型参数
- 可以重载函数模板,与普通函数差不多
- 调用模板函数之前请确保看清了没有个重载版本