1. 为什么需要函数模板
在泛型编程出现前,我们要实现一个swap函数得这样写:
void swap(int &a, int &b) { int tmp{a}; a = b; b = tmp; }
但这个函数只支持int型的变量交换,如果我们要做float, long, double, std::string等等类型的交换时,只能不断加入新的重载函数。这样做不但代码冗余,容易出错,还不易维护。C++函数模板有效解决了这个问题。函数模板摆脱了类型的限制,提供了通用的处理过程,极大提升了代码的重用性。
2. 函数模板使用
2.1 函数模板语法
函数模板作用:
建立一个通用函数,其函数返回值类型和形参类型可以不具体制定,用一个虚拟的类型来代表。
函数模板语法:
template<typename T> 函数声明或定义
template — 声明创建模板
typename — 表面其后面的符号是一种数据类型,可以用class代替
T — 通用的数据类型,名称可以替换,通常为大写字母
2.2 使用非类型形参
#include <iostream> // N必须是编译时的常量表达式 template<typename T, int N> void printArray(const T (&a)[N]) { std::cout << "["; const char *sep = ""; for (int i = 0; i < N; i++, (sep = ", ")) { std::cout << sep << a[i]; } std::cout << "]" << std::endl; } int main() { // T: int, N: 3 int a[] = {1,2,3}; printArray(a); } //输出:[1, 2, 3]
T (&a)[N]表明a是一个引用,其引用的数据类型是T[N],也即一个数组。
2.3 返回值为auto
有些时候我们会碰到这样一种情况,函数的返回值类型取决于函数参数某种运算后的类型。对于这种情况可以采用auto关键字作为返回值占位符。
template<typename T1, typename T2> auto multi(T a, T b) -> decltype(a * b) { return a * b; }
decltype操作符用于查询表达式的数据类型,也是C++11标准引入的新的运算符,其目的是解决泛型编程中有些类型由模板参数决定,而难以表示的问题。为何要将返回值后置呢?
// 这样是编译不过去的,因为decltype(a*b)中,a和b还未声明,编译器不知道a和b是什么。 template<typename T1, typename T2> decltype(a*b) multi(T a, T b) { return a*+ b; } //编译时会产生如下错误:error: use of undeclared identifier 'a'
2.4 类成员函数模板
函数模板可以做为类的成员函数。
#include <iostream> class object { public: template<typename T> void print(const char *name, const T &v) { std::cout << name << ": " << v << std::endl; } }; int main() { object o; o.print("name", "Crystal"); o.print("age", 18); }
输出:
name: Crystal age: 18
需要注意的是:函数模板不能用作虚函数。这是因为C++编译器在解析类的时候就要确定虚函数表(vtable)的大小,如果允许一个虚函数是函数模板,那么就需要在解析这个类之前扫描所有的代码,找出这个模板成员函数的调用或显式实例化操作,然后才能确定虚函数表的大小,而显然这是不可行的。
2.5 函数模板重载
函数模板之间、普通函数和模板函数之间可以重载。编译器会根据调用时提供的函数参数,调用能够处理这一类型的最佳匹配版本。在匹配度上,一般按照如下顺序考虑:
1 最符合函数名和参数类型的普通函数
2 特殊模板(具有非类型形参的模板,即对T有类型限制)
3 普通模板(对T没有任何限制的)
4 通过类型转换进行参数匹配的重载函数
#include <iostream> template<typename T> const T &max(const T &a, const T &b) { std::cout << "max(&, &) = "; return a > b ? a : b; } // 函数模板重载 template<typename T> const T *max(T *a, T *b) { std::cout << "max(*, *) = "; return *a > *b ? a : b; } // 函数模板重载 template<typename T> const T &max(const T &a, const T &b, const T &c) { std::cout << "max(&, &, &) = "; const T &t = (a > b ? a : b); return t > c ? t : c; } // 普通函数 const char *max(const char *a, const char *b) { std::cout << "max(const char *, const char *) = "; return strcmp(a, b) > 0 ? a : b; } int main() { int a = 1, b = 2; std::cout << max(a, b) << std::endl; std::cout << *max(&a, &b) << std::endl; std::cout << max(a, b, 3) << std::endl; std::cout << max("en", "ch") << std::endl; // 可以通过空模板实参列表来限定编译器只匹配函数模板 std::cout << max<>("en", "ch") << std::endl; }
编译输出:
max(&, &) = 2 max(*, *) = 2 max(&, &, &) = 3 max(const char *, const char *) = en max(*, *) = en
可以通过空模板实参列表来限定编译器只匹配函数模板,比如main函数中的最后一条语句
3. 函数模板不是函数
函数模板用来定义一族函数,而不是一个函数。C++是一种强类型的语言,在不知道T的具体类型前,无法确定swap需要占用的栈大小(参数栈,局部变量),同时也不知道函数体中T的各种操作如何实现,无法生成具体的函数。只有当用具体类型去替换T时,才会生成具体函数,该过程叫做函数模板的实例化。
当在main函数中调用swap(a,b)时,编译器推断出此时T为int,然后编译器会生成int版的swap函数供调用。所以相较普通函数,函数模板多了生成具体函数这一步。如果我们只是编写了函数模板,但不在任何地方使用它(也不显式实例化),则编译器不会为该函数模板生成任何代码。
4. 其它
4.1 函数模板 .vs. 模板函数
函数模板重点在模板。表示这是一个模板,用来生成函数。
模板函数重点在函数。表示的是由一个模板生成而来的函数。
4.2 cv限定
cv限定是指函数参数中有const、volatile或mutable限定。已指定、推导出或从默认模板实参获得所有模板实参时,函数参数列表中每次模板形参的使用都会被替换成对应的模板实参。替换后:
所有数组类型和函数类型参数被调整成为指针
所有顶层cv限定符从函数参数被丢弃,如在普通函数声明中。
顶层cv限定符的去除不影响参数类型的使用,因为它出现于函数中:
template <typename T> void f(T t); template <typename X> void g(const X x); template <typename Z> void h(Z z, Z *zp); // 两个不同函数有同一类型,但在函数中, t有不同的cv限定 f<int>(1); // 函数类型是 void(int) , t 为 int f<const int>(1); // 函数类型是 void(int) , t 为 const int // 二个不同函数拥有同一类型和同一 x // (指向此二函数的指针不相等,且函数局域的静态变量可以拥有不同地址) g<int>(1); // 函数类型是 void(int) , x 为 const int g<const int>(1); // 函数类型是 void(int) , x 为 const int // 仅丢弃顶层 cv 限定符: h<const int>(1, NULL); // 函数类型是 void(int, const int*) // z 为 const int , zp 为 int*