一、为什么要有函数模板
在泛型编程出现前,我们要实现一个swap函数得这样写:
void swap(int &a, int &b) {
int tmp{a};
a = b;
b = tmp;
}
但这个函数只支持int型的变量交换,如果我们要做float, long, double, std::string等等类型的交换时,只能不断加入新的重载函数。这样做不但代码冗余,容易出错,还不易维护。C++函数模板有效解决了这个问题。函数模板摆脱了类型的限制,提供了通用的处理过程,极大提升了代码的重用性。
二、什么是函数模板
cppreference中给出的定义是"函数模板定义一族函数",怎么理解呢?我们先来看一段简单的代码
#include <iostream>
template<typename T>
void swap(T &a, T &b) {
T tmp{a};
a = b;
b = tmp;
}
int main() {
int a = 2, b = 3;
swap(a, b); // 使用函数模板
std::cout << "a=" << a << ", b=" << b << std::endl;
}
swap支持多种类型的通用交换逻辑。它跟普通C++函数的区别在于其函数声明(declaration)前面加了个template<typename T>,这句话告诉编译器,swap中(函数参数、返回值、函数体中)出现类型T时,不要报错,T是一个通用类型。
函数模板的格式:
template<parameter-list> function-declaration
parameter-list是由英文逗号(,)分隔的列表,每项可以是下列之一:
序号 | 名称 | 说明 |
---|---|---|
1 | 非类型形参 | 已知的数据类型,如整数、指针等,C++11中有三种形式: int N int N = 1: 带默认值 int ...N: 模板参数包(可变参数模板) |
2 | 类型形参 | swap值用的形式,格式为: typename|class name[ = default] 或 typename|class ... name: 模板参数包 |
3 | 模板模板形参 | 没错有两个"模板",这个比较复杂,有兴趣的同学可以参考 cppreference之模板形参与模板实参 |
上面swap函数模板,使用了类型形参。函数模板就像是一种契约,任何满足该契约的类型都可以做为模板实参。而契约就是函数实现中,模板实参需要支持的各种操作。上面swap中T需要满足的契约为:支持拷贝构造和赋值。
template<typename T>
void swap(T &a, T &b) {
T tmp{a}; // 契约一:T需要支持拷贝构造
a = b; // 契约二:T需要支持赋值操作
b = tmp;
}
三、函数模板不是函数
刚才我们提到函数模板用来定义一族函数,而不是一个函数。C++是一种强类型的语言,在不知道T的具体类型前,无法确定swap需要占用的栈大小(参数栈,局部变量),同时也不知道函数体中T的各种操作如何实现,无法生成具体的函数。只有当用具体类型去替换T时,才会生成具体函数,该过程叫做函数模板的实例化。当在main函数中调用swap(a,b)
时,编译器推断出此时T
为int
,然后编译器会生成int版的swap函数供调用。所以相较普通函数,函数模板多了生成具体函数这一步。如果我们只是编写了函数模板,但不在任何地方使用它(也不显式实例化),则编译器不会为该函数模板生成任何代码。
函数模板实例化分为隐式实例化和显式实例化。
3.1 隐式实例化
仍以swap为例,我们在main中调用swap(a,b)
时,就发生了隐式实例化。当函数模板被调用,且在之前没有显式实例化时,即发生函数模板的隐式实例化。如果模板实参能从调用的语境中推导,则不需要提供。
#include <iostream>
template<typename T>
void print(const T &r) {
std::cout << r << std::endl;
}
int main() {
// 隐式实例化print<int>(int)
print(1);
// 实例化print<char>(char)
print<>('c');
// 仍然是隐式实例化,我们希望编译器生成print<double>(double)
print<double>(1);
}
3.2 显式实例化
在函数模板定义后,我们可以通过显式实例化的方式告诉编译器生成指定实参的函数。显式实例化声明会阻止隐式实例化。
template<typename R, typename T1, typename T2>
R add(T1 a, T2 b) {
return static_cast<R>(a + b);
}
// 显式实例化
template double add<double, int, double>(int, double);
// 显式实例化, 推导出第三个模板实参
template int add<int, int>(int, int);
// 全部由编译器推导
template double add(double, double);
如果我们在显式实例化时,只指定部分模板实参,则指定顺序必须自左至右依次指定,不能越过前参模板形参,直接指定后面的。
四、函数模板的使用
4.1 使用非类型形参
#include <iostream>
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
printArray({1, 2, 3});
}
//输出:[1, 2, 3]
4.2 返回值为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'
4.3 类成员函数模板
函数模板可以做为类的成员函数。
#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)的大小,如果允许一个虚函数是函数模板,那么编译器就需要在解析这个类之前扫描所有的代码,找出这个模板成员函数的调用或显式实例化操作,然后才能确定虚函数表的大小,而显然这是不可行的。
4.4 变参函数模板(模板参数包)
4.5 函数模板特化
五、其它
5.1 函数模板 .vs. 模板函数
函数模板重点在模板。表示这是一个模板,用来生成函数。
模板函数重点在函数。表示的是由一个模板生成而来的函数。
网友评论