函数模板
基本概念
通过函数模板可以一次定义一簇函数,这些函数共享着相同的代码逻辑,但是却可以处理不同的数据类型。
template<typename T>
T max(const T& a, const T& b)
{
return a > b ? a : b;
}
上面我们定义了一个函数模板max
,它除了有函数的参数声明外,还有模板参数声明。它在函数参数声明时没有指定具体类型,而是使用了const T&
,同时通过模板参数声明T
是一个类型:template<typename T>
。C++标准同时支持用class
关键字来声明模板的类型参数(注意struct
不行),但我们认为typename
的含义更加清楚,所以本书中一律使用typename
关键字。
T
的具体类型要等到对函数模板使用之时才进行推导。例如当我们调用int x = max(1, 2)
时,编译器通过传给max的实参1
和2
推导出T
的类型为int。在上例中,通过max的定义,约束了两个参数的类型必须一致(都为const T&),由于模板不会进行自动类型转换,所以当调用max(3, 4.5)
就会编译失败,提示仅仅通过T max(const T& a, const T& b)
无法实例化出函数T max(const int& a, const double& b)
。
当函数模板根据传入的参数,推导出具体类型后,就会根据具体类型代替模板参数,这以过程叫做实例化。例如当调用max(1,2)
时,编译器推导出T为int
,就会在背后产生一个如下的具体函数:
int max(const int& a, const int& b)
{
return a > b ? a : b;
}
同理当我们以不同类型的参数调用max,则会产生不同版本的具体max函数。所以可以得出结论,当没有任何人调用max函数模板时,在最终的目标代码中不会有任何的max实例化版本出现。编译器只会根据具体的调用情况,产生出适当的max实例化版本,也就是说目标代码中也可能有多份不同的max版本存在,所以在定义模板的时候尽量不要塞入太多和变化类型无关的代码,避免造成目标版本的过度膨胀。
由于对模板的使用存在实例化的过程,所以编译器对模板的处理可以认为大致分两步。第一步检查模板代码自身,主要查看语法是否正确,例如是否漏掉分号等。第二步是在实例化期间,对类型进行推导替换,判断类型推导是否可以成功,然后对实例化后的函数再做进一步的语法检查。
在函数模板的定义中声明的模板参数,除了可以在函数的参数声明中使用,同样可以在函数的定义体中使用。例如:
template<typename T1, typename T2>
T1 sum(T1 x, T2 y)
{
T1 total = x + y;
return total;
}
我们在函数模板中定义了临时变量total
,声明了它的类型为T1
,和第一个入参x的类型相同。所以在函数模板实例化时,根据函数的调用参数类型推导出T1的具体类型后,会对total的实际类型进行替换。
对于上例中sum的实现,我们希望编译器能够根据入参的类型T1和T2自动推导返回值total的类型,这样代码会更简洁且适应更多的场景。也就是说我们希望编译器能够根据类型T1和T2,自动推导T1+T2
的类型,该问题到了C++11之后语言自身给出了直接支持,具体我们放到后面的章节专门讨论。
对于函数模板,它的重载函数可以同时存在。例如对前面例子中max函数模板我们专门定义一个重载函数,用于对const char*
类型的字符串做比较。
const char* max(const char* a, const char* b)
{
return strcmp(a, b) > 0 ? a : b;
}
对于重载函数和模板函数,编译器会自动根据参数进行匹配,优先选择重载函数。我们可以认为是从特殊到普遍的选择过程,这和我们后面讲到的编译器对类模板的特化版本的选择过程基本是一致的。
函数模板在元编程中主要使用与下面两种场景:1)类型信息提取;2)类型判断。
对于下面的例子:
template <class T, size_t N>
inline T* begin_a(T(&arr)[N])
{
return arr;
}
template <class T, size_t N>
inline T* end_a(T(&arr)[N])
{
return arr + N;
}
对于传入的一个数组,begin_a
和end_a
可以分别获得数组的首尾地址。我们可以如下使用:
int arr[] = {0, 1, 3 /* ... */};
for(int p = begin_a(arr); p < end_a(arr); p++)
{
// do something with p
}
我们可以看到begin_a
和end_a
的入参类型声明都是T(&arr)[N]
,这要求入参都必须是数组类型,而且在实例化的过程中会自动推导出元素类型T和数组长度N。有了这两个信息,我们就可以在函数的定义中使用了。
C++11引入了auto和decltype专门来做类型推导,有了这两个关键字后,使用函数模板做类型推导的场景少了很多。但是auto和decltype的推导和函数模板的推导在细节上还是有不少差异之处,在后面现代C++对模板的改进的章节我们专门讲述。
对于使用函数模板做类型判断,经常利用的是编译器会优先选择重载函数,而后是模板函数。而使用重载函数时编译器会自动做默认类型转换,而对模板函数则不会。综合利用这两点,我们可以借助编译器来做类型的模式匹配。关于这一技巧,在后面元编程一章会专门讲述。