ARTICLE · 1132459
C++ 函数模板(Function Template)详解

1. 什么是函数模板?
函数模板是C++中一种强大的泛型编程工具,它允许我们编写与类型无关的通用函数。函数模板本质上是一个"函数的蓝图"或"公式",编译器可以根据使用时的具体类型,自动生成对应的函数实例。
核心思想:将函数中与类型相关的部分参数化,使得同一个算法可以适用于多种数据类型,而无需为每种类型重复编写代码。
举个直观的例子:我们想写一个返回两个数中较大值的函数。如果没有模板,需要为 int、double、char 等各写一个版本:
intmax(int a, int b){ return a > b ? a : b; }
doublemax(double a, double b){ return a > b ? a : b; }
charmax(char a, char b){ return a > b ? a : b; }
而使用函数模板,只需写一次:
template <typename T>
T max(T a, T b){
return a > b ? a : b;
}
2. 定义函数模板
函数模板的定义以关键字 template 开始,后跟用尖括号 <> 括起来的模板参数列表:
template <typename T>
返回类型 函数名(参数列表) {
// 函数体
}
template:声明这是一个模板typename T(或class T):声明一个类型参数TT可以在返回类型、参数列表和函数体中使用
注意:typename 和 class 在这里等价,都表示"任意类型"参数。现代C++更推荐用 typename。
3. 函数模板的简单用法
#include<iostream>
#include<string>
template <typename T>
T myMax(T a, T b){
return a > b ? a : b;
}
intmain(){
std::cout << myMax(3, 7) << '\n'; // T 推演为 int
std::cout << myMax(3.14, 2.71) << '\n'; // T 推演为 double
std::cout << myMax(std::string("apple"),
std::string("banana")) << '\n'; // T 推演为 std::string
return0;
}
输出:
7
3.14
banana
关键点:我们只调用 myMax(...),而不用写 myMax<int>(3, 7)。编译器会根据实参类型自动推演模板参数 T。这也正是许多C++初学者在用标准库模板(如 std::sort、std::max)时,根本没意识到自己在用模板的原因——因为语法上它和普通函数调用没有区别。
4. 模板实参推演
当调用 myMax(3, 7) 时,编译器尝试让 T 满足 T = int。推演规则大致如下:
若形参是 T,实参类型是int,则T = int。若形参是 const T&,实参是int,则T = int(顶层 const 被忽略)。若形参是 T&,实参是int,则T = int。
推演失败的情况:如果两个参数推演出不同的类型,编译器会报错:
myMax(3, 4.5); // 错误:T 既被推为 int 又被推为 double,冲突
显式指定模板参数
可以通过 <...> 显式指定模板参数,来消除歧义:
myMax<double>(3, 4.5); // T = double,3 被隐式转换为 double
myMax<int>(3, 4.5); // T = int,4.5 被截断为 4,返回 4
显式指定还有另一个重要场景:当模板参数只出现在返回值中时,无法通过实参推演:
template <typename T>
T createDefault(){
return T{};
}
int x = createDefault<int>(); // 必须显式指定,否则无法推演
std::string s = createDefault<std::string>();
5. 模板的实例化与组织
编译器并不会"执行"模板,而是在遇到具体调用时,根据模板生成一个具体的函数,这个过程称为**实例化(instantiation)**。
实例化是按需发生的:只有被用到的类型才会生成代码。 模板定义必须对使用点可见——这就是为什么模板的定义通常放在头文件中,而不是像普通函数那样把声明放头文件、定义放源文件。
// mymax.h
#ifndef MYMAX_H
#define MYMAX_H
template <typename T>
T myMax(T a, T b){
return a > b ? a : b;
}
#endif
如果模板定义和调用分处不同翻译单元,而定义不可见,就会出现链接错误(undefined reference)。
6. 函数模板的特化
有时某个类型需要不同于通用模板的处理方式,可以为它提供特化版本。
6.1 显式特化(全特化)
#include<cstring>
template <typename T>
boolisEqual(T a, T b){
return a == b;
}
// 为 const char* 提供特化版本,用 strcmp 比较字符串内容
template <>
bool isEqual<constchar*>(constchar* a, constchar* b) {
returnstd::strcmp(a, b) == 0;
}
intmain(){
std::cout << isEqual(3, 3) << '\n'; // 1(通用版本)
constchar* s1 = "hello";
constchar* s2 = "hello";
std::cout << isEqual(s1, s2) << '\n'; // 1(特化版本)
return0;
}
template <> 表示这是对所有模板参数都已确定的特化,即全特化。
7. 函数模板的重载
函数模板可以重载,也可以和普通函数共存:
#include<iostream>
#include<cstring>
// 普通函数
intmyMax(int a, int b){
std::cout << "[普通函数] ";
return a > b ? a : b;
}
// 函数模板
template <typename T>
T myMax(T a, T b){
std::cout << "[模板] ";
return a > b ? a : b;
}
// 另一个重载模板:三个参数
template <typename T>
T myMax(T a, T b, T c){
return myMax(myMax(a, b), c);
}
intmain(){
myMax(1, 2); // 非模板优先:调用普通函数
myMax(1.5, 2.5); // 普通函数不匹配:用模板
myMax(1, 2, 3); // 三参数模板
return0;
}
重载解析原则:
先收集所有可见的候选(普通函数 + 模板特化 + 由模板生成的实例)。 进行常规的重载解析,选出最优匹配。 如果普通函数和模板实例匹配程度相同,优先选择普通函数(非模板优先)。
8. 模板定义中的名字解析
模板中的名字分为两类:
依赖名字(dependent name):依赖于模板参数,在实例化时才能确定。 非依赖名字(non-dependent name):不依赖模板参数,在模板定义时即被解析。
template <typename T>
voidf(T x){
int n = 0; // 非依赖:定义时确定
g(n); // 若 g 是非依赖,则定义时就需要可见
typename T::value_type v; // 依赖:实例化时确定,需加 typename 消歧
}
实践要点:
依赖名字如果表示类型,需用 typename前缀消除歧义。依赖名字中调用成员函数模板时,若需指定模板参数,要用 template关键字:
template <typename T>
voidcall(T t){
t.template get<int>(); // 告诉编译器 get 是模板
}
9. 名字空间中的函数模板
模板可以定义在名字空间中,推荐做法是把模板放进项目的命名空间,避免污染全局作用域:
namespace mylib {
template <typename T>
T clamp(T v, T lo, T hi){
if (v < lo) return lo;
if (v > hi) return hi;
return v;
}
} // namespace mylib
intmain(){
std::cout << mylib::clamp(15, 0, 10) << '\n'; // 10
return0;
}
注意:名字空间会影响**ADL(实参依赖查找)**。当模板的实参类型位于某个名字空间时,该名字空间中的函数也会被纳入候选集。
10. 综合示例:通用数组求和与打印
下面用一个完整的例子,把本章讲到的多个要点串起来:模板定义、实参推演、显式模板参数、特化、重载、名字空间。
#include<iostream>
#include<string>
#include<cstring>
#include<numeric>
namespace util {
// 通用求和:适用于支持 += 和默认构造的类型
template <typename T>
T sum(const T* arr, std::size_t n){
T result{};
for (std::size_t i = 0; i < n; ++i) {
result += arr[i];
}
return result;
}
// 打印数组的通用版本
template <typename T>
voidprintArray(const T* arr, std::size_t n){
for (std::size_t i = 0; i < n; ++i) {
std::cout << arr[i] << (i + 1 == n ? '\n' : ' ');
}
}
// 针对 const char* 的特化:打印字符串内容,拼接字符串
template <>
constchar* sum<constchar*>(constchar* const* arr, std::size_t n) {
staticstd::string buffer;
buffer.clear();
for (std::size_t i = 0; i < n; ++i) {
buffer += arr[i];
}
return buffer.c_str();
}
} // namespace util
intmain(){
int ints[] = {1, 2, 3, 4, 5};
double dbls[] = {1.5, 2.5, 3.0};
std::cout << "整型数组: ";
util::printArray(ints, 5);
std::cout << "求和 = " << util::sum(ints, 5) << '\n';
std::cout << "浮点数组: ";
util::printArray(dbls, 3);
std::cout << "求和 = " << util::sum(dbls, 3) << '\n';
constchar* strs[] = {"Hello, ", "template ", "world!"};
std::cout << "字符串数组: ";
util::printArray(strs, 3);
std::cout << "拼接 = " << util::sum(strs, 3) << '\n';
return0;
}
可能的输出:
整型数组: 1 2 3 4 5
求和 = 15
浮点数组: 1.5 2.5 3
求和 = 7
字符串数组: Hello, template world!
拼接 = Hello, template world!
小结
template <typename T> T f(T a) | |
<T>,编译器自动推演 | |
<T> 显式指定 | |
template <> | |
typename/template 消歧 | |
函数模板是C++泛型编程的基石。掌握它之后,再学习类模板、可变参数模板、SFINAE、Concepts 等高级主题就会水到渠成。建议读者从改写手头的"重复函数"开始,亲手把若干重载函数合并成一个模板,体会泛型编程带来的简洁与威力。