函数模板是 C++ 泛型编程的基础。本文从「为什么需要模板」讲起,依次介绍模板的定义、实例化、多类型参数、重载与特化,最后整理几个初学者常踩的坑。

一、为什么需要函数模板

假设要写一个求两个数较大值的函数,不用模板就得为每种类型重载一遍:

1
2
3
4
int max(int a, int b)        { return a > b ? a : b; }
double max(double a, double b) { return a > b ? a : b; }
char max(char a, char b) { return a > b ? a : b; }
// 每多一种类型就要再写一遍,逻辑完全相同……

这些函数的逻辑一模一样,只是参数类型不同。函数模板让我们只写一份代码,由编译器根据调用时的类型自动生成对应版本。

二、定义语法

1
2
template <class T>    // class 是 C++ 关键字,在这里表示「任意类型」
template <typename T> // typename 是 C++11 引入的写法,含义与 class 完全相同,推荐使用

class T 里的 class 并不代表 T 必须是类,它可以是 int、double 等任意类型。两种写法没有任何区别,现代 C++ 更推荐用 typename,语义更清晰。

一个完整的例子:

1
2
3
4
template <typename T>
T myMax(T a, T b) {
return a > b ? a : b;
}

注意 T a, T b 中 T 和变量名之间必须有空格,写成 Tb 会被编译器当成一个标识符,直接报错。

三、隐式实例化(implicit instantiation)

模板本身不是函数,它只是一张「图纸」。当我们用具体类型调用它时,编译器才按需生成真正的函数,这叫隐式实例化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>
using namespace std;

template <typename T>
T myMax(T a, T b) {
return a > b ? a : b;
}

int main() {
cout << myMax(3, 5) << endl; // 编译器生成 myMax<int>
cout << myMax(2.5, 1.7) << endl; // 编译器生成 myMax<double>
cout << myMax('a', 'z') << endl; // 编译器生成 myMax<char>
return 0;
}

优点:只有真正用到的类型才会生成对应函数,没有用到的类型不产生任何代码。

也可以在调用时手动指定类型参数(实参类型不一致时很有用):

1
2
myMax<int>(3, 5);      // 显式指定 T = int
myMax<double>(3, 5.2); // 一个 int 一个 double,靠推导会失败,手动指定后 int 会隐式转换

四、显式实例化(explicit instantiation)

除了靠调用触发,也可以主动命令编译器生成某个类型的版本:

1
template float myMax<float>(float a, float b); // 即使没有调用,也生成 myMax<float>

它的用途主要有两个:

  1. 提前编译、分离声明与实现:把模板实现放在 .cpp 文件里时,配合显式实例化可以避免「未定义的引用」链接错误;
  2. 减少重复实例化带来的编译开销。

新手阶段几乎用不到显式实例化,知道有这回事即可。注意是「显式」不是「显示」。

五、多个类型参数

模板可以同时声明多个类型参数:

1
2
3
4
template <typename T1, typename T2>
T1 myMax(T1 a, T2 b) {
return a > b ? a : b;
}

调用时 myMax(3, 5.2) 中 T1 = int、T2 = double。不过要注意:返回类型是 T1,如果返回的其实是 b(double),结果会被截断成 T1 类型,可能丢失精度。C++11 之后可以用 auto 让编译器推导返回类型来避免这个问题。

六、函数模板重载

函数模板和普通函数一样,可以靠参数个数不同进行重载:

1
2
3
4
5
6
7
8
9
10
template <typename T>
T myMax(T a, T b) { // 两个参数版本
return a > b ? a : b;
}

template <typename T>
T myMax(T a, T b, T c) { // 三个参数版本
T temp = a > b ? a : b;
return c > temp ? c : temp;
}

调用时编译器会根据参数个数自动匹配。

七、函数模板特化(specialization)

有时通用模板对某些类型不适用,需要为特定类型单独写一份实现,这就是特化。

比如通用模板直接比较两个 const char*,比的其实是字符串的地址而不是字典序,这是错的。我们为它特化一个用 strcmp 比较内容的版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <cstring>

// 通用模板
template <typename T>
T myMax(T a, T b) {
return a > b ? a : b;
}

// 针对 const char* 的特化版本
template <>
const char* myMax<const char*>(const char* a, const char* b) {
return strcmp(a, b) > 0 ? a : b; // 按字典序比较字符串内容
}

特化的规则:

  • 语法上,template <> 后面紧跟函数,并用 <具体类型> 标明特化的类型;
  • 当传入参数类型完全匹配特化版本时,编译器优先使用特化函数;
  • 类型不完全匹配时(例如传了 char* 而不是 const char*),仍会使用通用模板实例化。

八、常见的坑

  1. 模板实现通常要放在头文件(.h/.hpp)里。模板在实例化时必须看到完整定义,只有声明会报链接错误。这和普通函数「声明放 .h、实现放 .cpp」的习惯不同,新手最容易在这里卡住。
  2. 慎用 max 这个名字。如果写了 using namespace std;,自己定义的 max 可能和标准库的 std::max 冲突,报一堆莫名其妙的错误。学习时建议命名成 myMax 这类名字(本文示例就是这么做的)。
  3. 类型参数推导不会做隐式转换。myMax(3, 5.2) 会因为 T 同时被推导成 int 和 double 而报错,需要写成 myMax<double>(3, 5.2) 或强转参数。
  4. 模板报错信息通常又长又难懂,先检查「类型是否匹配」和「该类型是否支持模板里用到的运算符」(比如 >)。

九、小结

  • 函数模板用一份代码支持任意类型,避免为每种类型重复写重载;
  • template <typename T> 与 template <class T> 等价,推荐前者;
  • 隐式实例化靠调用自动生成函数,显式实例化用 template 类型 函数名<类型>(...) 主动生成;
  • 模板支持多个类型参数、重载,以及用 template <> 为特殊类型做特化;
  • 模板定义一般放头文件;自定义函数避开 max 等标准库名字。