函数模板
函数模板被称为”生成代码的代码”,是 C++ 通用编程的核心工具,它通过参数化类型定义一族函数的蓝图,编译时会根据实际使用的类型 / 值自动生成具体函数实例,无需为每种场景重复编写相同逻辑,大幅提升代码复用性与扩展性。
函数模板的声明 / 定义需遵循特定语法,C++ 标准演进中新增了简化语法,同时废弃了部分特性,核心语法分为以下几类:
函数模板的语法
标准语法(C++98 起)
基础语法通过template关键字声明模板参数列表,后接函数声明 / 定义,适用于所有 C++ 标准。
1 | // 语法格式:template <参数列表> 函数声明/定义 |
C++20 新增语法
C++20 引入约束(constraints) 和缩写函数模板,简化模板定义并增强类型检查。
带约束的模板:用
requires限定模板参数需满足的条件(依赖概念库)1
2
3
4
5template <typename T>
requires std::integral<T> // 约束T为整数类型
T add(T a, T b) {
return a + b;
}缩写函数模板:用
auto或Concept auto作为参数类型占位符,隐式生成模板参数1
2
3
4
5
6
7void print(auto value) { // 等价于template <typename T> void print(T value)
std::cout << value << std::endl;
}
void sum(std::floating_point auto a, std::floating_point auto b) { // 约束为浮点类型
std::cout << a + b << std::endl;
}
废弃语法
export关键字(C++11 前允许,C++11 起移除)曾用于声明 “导出模板”,允许跨文件实例化,但因实现复杂且兼容性差被废弃,现无需使用。
模板实参的推演
在编译时期,函数模板通过使用typedef关键字对不同类型的参数进行重命名
1 | template <class T> |
函数模板根据提供的实参推演出形参的类型,生成不同的函数进行编译
含不同类型实参的模板
1
2
3
4
5
6
7
8
9
10 template <class T,class U>
auto Max(const T& a, const U& b) {
return (a > b) ? a : b;
}
int main() {
int x = 10;
double y = 20.5;
cout << "Max of " << x << " and " << y << " is: " << Max(x, y) << endl;
return 0;
}C++17后引入
auto的概念,使得返回类型可以自行推演
- 优势:通过模板生成对应函数,泛型编程
- 劣势:类型名越多,生成的代码越多,导致代码体量膨胀
模板参数类型
函数模板的参数列表支持三类参数,可组合使用:
- 类型参数:用
typename/class声明,代表一个未指定的类型(如template <typename T>中的T)。 - 非类型参数:代表一个编译期已知的常量值,类型需为整数类型、枚举、指针或引用(如
template <int N>中的N)。 - 参数包(C++11 起):用
...声明,可接受任意数量的同类型参数(如template <typename... Args>中的Args),配合折叠表达式使用。
编译时实例化
函数模板本身不生成代码,仅在 “需要使用” 时由编译器生成具体实例(实例化),分为显式实例化和隐式实例化两种方式。
- 实例化过程:编译器根据传入的模板实参,替换模板中的参数占位符,生成独立的函数代码(如
max<int>(1,2)生成int max(int a, int b))。 - 特点:实例化发生在编译期,无运行时开销,且类型错误可在编译期发现。
默认模板参数
C++11 起允许为模板参数指定默认值,使用时若未显式传入实参,则采用默认值。
1 | template <typename T = int, int Size = 10> // T默认int,Size默认10 |
函数模板的实例化
实例化是将模板转化为具体函数的过程,根据触发方式分为显式实例化和隐式实例化。
显式实例化
开发者主动指定模板实参,强制编译器生成实例,适用于需控制实例化位置(如减少重复编译)的场景。
显式实例化定义:生成实例代码,程序中同一参数列表的实例只能定义一次。
1
2
3
4
5template <typename T> T max(T a, T b) { return a > b ? a : b; }
// 显式实例化max<int>
template int max<int>(int a, int b);
template int max<>(int a, int b); // 实参可推导时,<>内可省略显式实例化声明(C++11 起):用
extern声明 “实例在其他文件中定义”,阻止当前文件隐式实例化,避免重复生成代码。1
2
3
4
5// a.h中声明
extern template int max<int>(int a, int b);
// a.cpp中定义
template int max<int>(int a, int b);
隐式实例化
编译器在 “需要使用模板函数” 时自动推导实参并生成实例,无需开发者干预,是最常用的实例化方式。
触发场景:函数调用、取函数地址、初始化函数指针等。
1
2
3
4
5
6
7
8template <typename T> void print(T value) { std::cout << value << std::endl; }
int main() {
print(10); // 隐式实例化print<int>
print(3.14); // 隐式实例化print<double>
void (*fp)(std::string) = print; // 隐式实例化print<std::string>
return 0;
}注意:C++11 起,若表达式需常量求值(如
constexpr函数),即使未直接调用,也可能触发隐式实例化。
模板实参推导与替换
模板实参推导
编译器根据函数调用的实参类型,自动推导模板参数的过程,无需显式指定所有实参(部分场景需显式指定,如返回类型无法推导)。
推导规则:实参类型需与模板参数类型 “兼容”,忽略顶层
const/volatile,数组和函数类型会退化为指针。1
2
3
4
5
6
7template <typename To, typename From> To convert(From f) { return static_cast<To>(f); }
int main() {
int i = convert<int>(3.14); // From推导为double,To显式指定为int
char c = convert<char>(65); // From推导为int,To显式指定为char
return 0;
}运算符模板的推导:重载运算符(如
<<)无法显式指定模板实参,需依赖推导(如std::cout << "hello"推导operator<<<char>)。
模板实参替换
推导得到模板实参后,编译器将模板中的参数占位符替换为具体实参,生成实例代码。
替换失败:若替换后代码非法(如调用不存在的成员函数),编译器不会报错,而是将该模板从重载集中移除,此机制称为SFINAE(替换失败不是错误),是模板元编程的基础。
1
2
3
4// 仅当T有size()成员函数时,此模板才有效(否则替换失败,被移除)
template <typename T> auto getSize(const T& t) -> decltype(t.size(), void()) {
std::cout << t.size() << std::endl;
}
函数模板的重载
函数模板可与非模板函数、其他函数模板重载,编译器通过重载决议选择最佳匹配,核心规则如下:
重载的形式
模板与非模板重载:非模板函数优先于模板特化(若类型完全匹配)。
1
2
3
4
5
6
7
8void print(int x) { std::cout << "非模板:" << x << std::endl; }
template <typename T> void print(T x) { std::cout << "模板:" << x << std::endl; }
int main() {
print(10); // 调用非模板函数(完全匹配)
print(3.14); // 调用模板函数(推导为double)
return 0;
}多模板重载:通过部分排序判断 “更特化” 的模板,更特化的模板优先匹配(“更特化” 指接受的类型范围更小,如
T*比T更特化)。1
2
3
4
5
6
7
8
9template <typename T> void f(T x) { std::cout << "T" << std::endl; }
template <typename T> void f(T* x) { std::cout << "T*" << std::endl; }
int main() {
int a = 10;
f(a); // 调用f(T)(T推导为int)
f(&a); // 调用f(T*)(T推导为int,更特化)
return 0;
}
部分排序规则
判断两个模板 “谁更特化” 的核心逻辑:将一个模板的参数 “虚构类型” 代入另一个模板,若能推导成功,则后者更通用,前者更特化。
- 示例:比较
f(T)和f(T*):- 将
f(T*)的虚构类型U*代入f(T),推导T=U*,成功 →f(T)更通用。 - 将
f(T)的虚构类型U代入f(T*),推导T*=U,失败 →f(T*)更特化。 - 结论:
f(T*)优先于f(T)匹配指针类型实参。
- 将
函数模板的特化
函数模板特化是为特定模板实参,提供定制化的实现(区别于重载,特化不是新增函数,而是为已有模板提供 “特殊实例”)。
显式特化
为特定模板实参显式定义实现,语法为template<> 函数声明。
1 | // 主模板 |
特化与重载的区别
- 特化:不改变模板的 “家族关系”,仅为特定实参定制实现,需依赖主模板存在。
- 重载:新增独立的函数(模板或非模板),与原模板无 “从属关系”,重载决议时独立参与匹配。
- 关键注意:编译器先通过重载决议选择主模板,再检查该主模板是否有特化,而非直接选择特化。



