在 C++ 中,“函数模板(function template)” 是一种通用的函数定义,它使用类型参数来代替固定的数据类型。当多个函数的处理步骤相同、仅仅是参数或返回值类型不同时,编写一份函数模板就能适配多种类型,省去重复编写重载函数的麻烦。
C++ 函数模板语法
定义函数模板时,我们需要在普通函数定义的正上方加上一行 template <typename T> 声明,并把函数中需要随类型改变的位置替换为类型参数 T。
语法:
template <typename T>
返回类型 函数名(参数列表) {
// 函数体
}说明:
在上面的语法中,template 关键字表示接下来要定义一个模板,尖括号里的 typename T 用于声明一个名为 T 的 “类型参数”。凡是在函数返回类型、参数列表或函数体内部需要用到该可变类型的地方,都可以直接使用 T 来占位。
另外,早期的 C++ 代码中也常把类型参数写成 template <class T>,它与 template <typename T> 完全等价,日常开发中我们更推荐字面意思直观的 typename T。在使用函数模板语法时,小伙伴们还需要注意以下 2 点:
- 类型参数名可以自定义:
T并不是固定死的关键字,平时习惯写T只是因为它是英文 “Type(类型)” 的首字母,我们也可以将其命名为ItemType等合法的标识符。 - 每份模板声明只管一个函数:每一行
template <typename T>声明只对紧跟在它下方的那一个函数生效。如果后面还要定义第二个函数模板,就必须重新写一遍template <typename T>。
对于第二点,很多初学的小伙伴在连续编写多个函数模板时容易漏写,我们可以对比一下下面的正确与错误写法:
// 正确方式
template <typename T>
T maxVal(T a, T b) {
return a > b ? a : b;
}
template <typename T>
T minVal(T a, T b) {
return a < b ? a : b;
}
// 错误方式
template <typename T>
T maxVal(T a, T b) {
return a > b ? a : b;
}
T minVal(T a, T b) {
return a < b ? a : b;
}在上面的错误方式中,第二个函数 minVal() 上方漏掉了 template <typename T>,编译器读到 minVal() 里的 T 时认不出它是什么类型,就会直接报错。下面我们来看一个完整的例子。
示例 1:求两个数的最大值
#include <iostream>
// 求两个数的最大值
template <typename T>
T maxValue(T a, T b) {
return a > b ? a : b;
}
int main() {
std::cout << maxValue(10, 20) << '\n';
std::cout << maxValue(3.5, 2.1) << '\n';
std::cout << maxValue('A', 'Z') << '\n';
return 0;
}运行结果如下。
20
3.5
Z分析:
在这个例子中,我们只编写了一份 maxValue() 函数模板,随后在 main() 函数中依次传入了 int、double 和 char 三种类型的实参。其中传入字符 'A' 和 'Z' 时,程序会按照字符对应的 ASCII 码值比较大小,最终得出 'Z'。
编译器在编译阶段看到这三行调用代码时,会自动根据传入的实参类型,分别生成 int、double 和 char 三个具体版本的函数。
“函数模板” 与 “模板函数” 的区别
在阅读一些 C++ 资料时,我们经常会看到 “函数模板” 和 “模板函数” 这两个很像的词,其实重点看最后两个字就能轻松区分:
函数模板:落脚在 “模板” 上,指的是我们编写的那份带 template <typename T> 的通用代码图纸。
模板函数:落脚在 “函数” 上,指的是编译器根据模板替换具体类型后生成的真实函数,比如针对 int 生成的具体函数版本。
函数模板的调用方式
在 C++ 中,定义好函数模板后,调用时主要有 2 种方式:① 自动类型推导;② 显式指定模板类型。下面我们就来看看这两种方式的具体用法。
1. 自动类型推导(推荐)
就像示例 1 演示的那样,当调用处传入的实参类型明确且一致时,我们不需要写尖括号告诉编译器 T 是什么类型,直接像调用普通函数一样写 函数名(实参) 就可以了,编译器会自动推导出 T 的具体类型。
此外,函数模板的形参不仅可以使用普通的值传递 T,也可以结合前面学过的引用传递,声明为引用类型 T& 或常量引用类型 const T&。下面我们就使用 T& 来编写一个通用的交换函数模板 mySwap()。
示例 2:交换两个变量的值
#include <iostream>
#include <string>
// 交换两个变量的值
template <typename T>
void mySwap(T& a, T& b) {
T temp = a;
a = b;
b = temp;
}
int main() {
int x = 10;
int y = 20;
mySwap(x, y);
std::cout << x << " " << y << '\n';
std::string s1 = "lvye";
std::string s2 = "lvyenet";
mySwap(s1, s2);
std::cout << s1 << " " << s2 << '\n';
return 0;
}运行结果如下。
20 10
lvyenet lvye分析:
void mySwap(T& a, T& b) 将两个形参声明为引用类型 T&,并在内部使用 T 定义了临时变量 temp。无论是传入 int 变量还是 std::string 变量,编译器都能自动推导出对应的类型,并直接交换原变量的值。
需要注意的是,形参中的引用符号(&)是不能省略的。如果写成普通的值传递 void mySwap(T a, T b),函数内部交换的就只是形参副本,外部的 x 和 y 并不会发生改变。
2. 显式指定模板类型
自动类型推导用起来非常省事,但在有些情况下,编译器无法单靠实参推导出 T 的类型。这时我们就需要在函数名后面的尖括号 “<>” 中显式写出具体的数据类型。
语法:
函数名<具体类型>(实参列表);说明:
在实际开发中,需要显式指定模板类型的主要有以下 2 种情况:
- 多个实参对应的类型不一致:比如函数模板形参为
(T a, T b),调用时却同时传入了两种不同类型的实参,编译器无法确定T应该选哪一种。 - 模板参数未出现在形参列表中:比如
T只作为函数的返回值类型,小括号里的实参根本不包含T的类型信息,编译器无从推导。
示例 3:不同类型实参求最大值
#include <iostream>
template <typename T>
T maxValue(T a, T b) {
return a > b ? a : b;
}
int main() {
// 自动推导 T 为 int
std::cout << maxValue(10, 20) << '\n';
// 显式指定 T 为 double
std::cout << maxValue<double>(3, 7.5) << '\n';
return 0;
}运行结果如下。
20
7.5分析:
对于 maxValue(T a, T b) 来说,如果我们直接写 maxValue(3, 7.5),第 1 个实参 3 是 int,第 2 个实参 7.5 是 double,编译器会因为无法统一 T 的类型而直接报错。
可能有些小伙伴会问:“普通函数不是会自动把 int 转成 double 吗?” 其实普通函数的形参类型是提前固定好的,而函数模板在自动推导时 T 本身还没确定下来,所以要求同一个 T 对应的实参类型必须一致。
当我们显式写成 maxValue<double>(3, 7.5) 后,T 就被固定为了 double。此时实参 3 会通过隐式类型转换变成 3.0,再与 7.5 比较得出 7.5。反过来,下面这行代码最终输出的结果是 3,小伙伴们可以思考一下为什么。
std::cout << maxValue<int>(3.9, 2.1) << '\n';当然了,如果希望函数模板不写 <double> 也能直接接收两个不同类型的实参,我们还可以在定义模板时声明两个独立的类型参数,这一点会在后面的 “C++ 模板参数” 一节中展开介绍。
再来看第二种情况:当类型参数 T 只作为函数的返回值类型、没有出现在形参列表中时,调用时也必须通过 <具体类型> 显式指明。下面来看一个例子。
示例 4:指定返回值的模板类型
#include <iostream>
template <typename T>
T getHalf(int num) {
T val = num;
return val / 2;
}
int main() {
std::cout << getHalf<int>(9) << '\n';
std::cout << getHalf<double>(9) << '\n';
return 0;
}运行结果如下。
4
4.5分析:
在 getHalf() 函数模板中,形参 num 的类型固定是 int,而类型参数 T 只用来决定返回值和内部变量 val 的类型。如果调用时只写 getHalf(9),编译器单看实参 9 无法判断返回值 T 应该使用什么类型,编译时就会报错。
当我们显式写成 getHalf<int>(9) 时,val 是整数 9,9 / 2 执行整数除法并截断小数,结果为 4。而写成 getHalf<double>(9) 时,val 变为浮点数 9.0,9.0 / 2 保留小数部分,结果为 4.5。
常见问题
1. 既然有了函数模板,平时还需要使用普通函数重载吗?
需要。两者的适用场景不同:只有当多个函数的 “处理步骤和代码结构完全相同,仅仅是数据类型不同” 时,才适合使用函数模板。如果不同参数个数或不同数据类型需要执行完全不同的业务逻辑,使用普通的函数重载反而更加清晰直观。
2. 如果普通函数和函数模板同名且都能匹配,编译器会调用哪一个?
假设代码中同时定义了普通函数 void show(int a) 和函数模板 template <typename T> void show(T a),当调用 show(10); 时,编译器遵循以下规则:
优先调用普通函数:直接写 show(10); 时,编译器会优先选择现成的普通函数,省去模板实例化的过程。
空尖括号调用函数模板:如果希望在这种同名情况下指定调用函数模板,可以在函数名后面加上一对空尖括号,写成 show<>(10);。
