在上节课(函数模板)中,我们介绍了函数模板,并把普通的 max
函数改写为模板:
template <typename T>
T max(T x, T y)
{
return (x < y) ? y : x;
}
本课将聚焦于函数模板的使用方式。
使用函数模板
函数模板本身并非真正的函数——其代码不会被直接编译或执行。它的唯一职责是生成函数(这些生成的函数才会被编译和执行)。
调用 max<T>
模板需使用如下语法:
max<实际类型>(arg1, arg2); // 实际类型如 int 或 double
与常规函数调用几乎相同,区别只是在尖括号中指定模板实参,告诉编译器用何种具体类型替换模板形参 T
。
示例:
#include <iostream>
template <typename T>
T max(T x, T y)
{
return (x < y) ? y : x;
}
int main()
{
std::cout << max<int>(1, 2) << '\n'; // 实例化并调用 max<int>(int, int)
return 0;
}
当编译器遇到 max<int>(1, 2)
时,发现尚未定义 max<int>(int, int)
,于是隐式地利用模板生成该函数。
从模板生成具体类型函数的过程称为函数模板实例化(instantiation)。由函数调用触发的实例化称为隐式实例化。生成的函数称为特化(specialization),俗称函数实例;原模板则称为主模板。实例化后的函数与普通函数完全一致。
术语说明
“特化”一词更常指显式特化,即手动为模板定义特定版本的函数(见 26.3 课)。
实例化流程简单描述:编译器复制主模板,将模板形参 T
替换为指定的具体类型(如 int
)。
调用 max<int>(1, 2)
时,生成的特化大致如下:
template<>
int max<int>(int x, int y)
{
return (x < y) ? y : x;
}
完整编译后,程序等价于:
#include <iostream>
template <typename T>
T max(T x, T y);
template<>
int max<int>(int x, int y)
{
return (x < y) ? y : x;
}
int main()
{
std::cout << max<int>(1, 2) << '\n';
return 0;
}
- 每个翻译单元中,模板在首次调用时实例化一次;后续调用复用已生成的函数。
- 若无调用,则该翻译单元不会实例化该模板。
再举一例:
#include <iostream>
template <typename T>
T max(T x, T y)
{
return (x < y) ? y : x;
}
int main()
{
std::cout << max<int>(1, 2) << '\n'; // 实例化 max<int>
std::cout << max<int>(4, 3) << '\n'; // 复用 max<int>
std::cout << max<double>(1, 2) << '\n'; // 实例化 max<double>
return 0;
}
最终程序等价于:
// 模板声明
template <typename T>
T max(T x, T y);
// 生成的特化
template<>
int max<int>(int x, int y) { ... }
template<>
double max<double>(double x, double y) { ... }
注意:实例化 max<double>
时形参类型为 double
,而我们传入 int
,实参会被隐式转换为 double
。
模板实参推导
当实参类型与希望实例化的类型一致时,可省略显式指定,让编译器根据实参类型推导模板实参:
std::cout << max<int>(1, 2); // 显式指定
std::cout << max<>(1, 2); // 仅考虑模板,推导为 max<int>
std::cout << max(1, 2); // 普通调用语法,推导为 max<int>
max<>(1, 2)
:仅考虑模板重载,推导max<int>
。max(1, 2)
:同时考虑模板和非模板重载;若两者同样匹配,优先非模板函数。
示例:
#include <iostream>
template <typename T>
T max(T x, T y)
{
std::cout << "max<T>\n";
return (x < y) ? y : x;
}
int max(int x, int y)
{
std::cout << "max(int,int)\n";
return (x < y) ? y : x;
}
int main()
{
max<int>(1, 2); // 模板
max<>(1, 2); // 模板(非模板不参与)
max(1, 2); // 非模板优先
}
最佳实践
除非需要强制使用模板版本,优先使用普通函数调用语法。
模板与普通形参混用
模板也可含非模板形参:
template <typename T>
int someFcn(T, double) // double 为非模板形参
{
return 5;
}
调用示例:
someFcn(1, 3.4); // T=int, double 已匹配
someFcn(1.2f, 3.4); // float 提升为 double,T=float
实例化后的函数可能无法编译
模板只要语法合法即可实例化,但语义是否合理需程序员保证。
示例:
template <typename T>
T addOne(T x) { return x + 1; }
int main()
{
addOne("Hello"); // 编译 OK,但语义错误(指针偏移)
}
可显式禁止某些实例化:
template <>
const char* addOne<const char*>(const char*) = delete;
静态局部变量与模板
模板函数中的 static
局部变量,每个实例化版本各有一份:
template <typename T>
void printID(T v)
{
static int id = 0;
std::cout << ++id << ") " << v << '\n';
}
printID(12); // id=1(int版)
printID(13); // id=2(int版)
printID(14.5); // id=1(double版)
泛型编程
因模板类型可替换为任意实际类型,又称泛型编程(generic programming)。它让我们聚焦算法与数据结构,而非具体类型。
结论
函数模板写起来与普通函数几乎一样,却能显著减少重复代码与维护成本。
缺点:
- 每遇到新类型组合就生成一份函数,可能导致代码膨胀与编译时间增加;
- 错误信息往往冗长难读。
权衡之下,优势远大于缺点。建议:先写普通函数,发现需要不同参数类型时再升级为模板。
最佳实践
需要跨类型通用算法时,优先使用函数模板。