函数模板
定义与作用
函数模板是参数化的函数蓝图——用类型或值作为参数,编译器在调用时自动或显式地生成具体函数实例:
template <typename T>
T max(T a, T b) {
return a > b ? a : b;
}
int i = max(3, 5); // 实例化 max<int>
double d = max(3.14, 2.71); // 实例化 max<double>
它的核心价值:一次编写,多类型复用——避免为每种类型手写一个重载版本。
核心原理
模板实例化流程
两阶段查找
完整示例
示例一:泛型数据统计工具
场景说明:一个数据统计类,包含求最大值、求和、平均值等泛型函数。
#include <iostream>
#include <vector>
#include <string>
#include <numeric>
#include <type_traits>
// 1. 基本函数模板:最大值
template <typename T>
T maximum(T a, T b) {
return a > b ? a : b;
}
// 2. 三个参数的重载
template <typename T>
T maximum(T a, T b, T c) {
return maximum(maximum(a, b), c);
}
// 3. 模板 + 非模板参数:数组求和
template <typename T>
T sum(const std::vector<T>& data) {
T total = T{}; // 值初始化(int→0, double→0.0)
for (const auto& v : data) {
total += v;
}
return total;
}
// 4. 返回类型声明:平均值(用后置返回类型自动推导)
template <typename T>
auto average(const std::vector<T>& data) -> decltype(T{} / data.size()) {
if (data.empty()) return T{};
return sum(data) / static_cast<T>(data.size());
}
// 5. 多个模板参数:两容器相等比较
template <typename T1, typename T2>
bool container_equal(const std::vector<T1>& a, const std::vector<T2>& b) {
if (a.size() != b.size()) return false;
for (size_t i = 0; i < a.size(); ++i) {
if (a[i] != static_cast<T1>(b[i])) return false;
}
return true;
}
// 6. 非类型模板参数:固定大小数组的泛型打印
template <typename T, size_t N>
void print_array(const T (&arr)[N]) {
std::cout << "[";
for (size_t i = 0; i < N; ++i) {
if (i > 0) std::cout << ", ";
std::cout << arr[i];
}
std::cout << "]" << std::endl;
}
int main() {
std::cout << "===== 基本函数模板 =====" << std::endl;
std::cout << "max(3, 7) = " << maximum(3, 7) << std::endl;
std::cout << "max(3.14, 2.71) = " << maximum(3.14, 2.71) << std::endl;
std::cout << "max('a', 'z') = " << maximum('a', 'z') << std::endl;
std::cout << "max(1, 5, 3) = " << maximum(1, 5, 3) << std::endl;
// 显式指定模板参数(避免歧义)
std::cout << "max<double>(3, 4.5) = " << maximum<double>(3, 4.5) << std::endl;
std::cout << "\n===== 容器统计 =====" << std::endl;
std::vector<int> scores = {85, 92, 78, 95, 88};
std::cout << "总分: " << sum(scores) << std::endl;
std::cout << "平均分: " << average(scores) << std::endl;
std::vector<double> prices = {19.99, 29.50, 15.75};
std::cout << "总价: " << sum(prices) << std::endl;
std::cout << "均价: " << average(prices) << std::endl;
std::cout << "\n===== 多类型模板参数 =====" << std::endl;
std::vector<int> a = {1, 2, 3};
std::vector<double> b = {1.0, 2.0, 3.0};
std::cout << "containers equal? " << std::boolalpha
<< container_equal(a, b) << std::endl;
std::cout << "\n===== 非类型模板参数 =====" << std::endl;
int arr[] = {10, 20, 30, 40, 50};
print_array(arr);
return 0;
}
预期输出:
===== 基本函数模板 =====
max(3, 7) = 7
max(3.14, 2.71) = 3.14
max('a', 'z') = z
max(1, 5, 3) = 5
max<double>(3, 4.5) = 4.5
===== 容器统计 =====
总分: 438
平均分: 87.6
总价: 65.24
均价: 21.7467
===== 多类型模板参数 =====
containers equal? true
===== 非类型模板参数 =====
[10, 20, 30, 40, 50]
逐段分析:
maximum(T a, T b)是最基础的函数模板——T 从实参推导,两个参数类型必须一致maximum<double>(3, 4.5)显式指定 T=double——解决int和double的推导歧义auto average(...) -> decltype(...)用后置返回类型 + decltype 推导返回类型——比直接写double更泛用container_equal有两个独立的类型参数T1和T2——允许比较 int 向量和 double 向量print_array使用非类型模板参数size_t N——编译期推导数组大小,无运行时开销
示例二:swap 函数模板的设计
#include <iostream>
#include <vector>
#include <string>
// 泛型 swap——适用于任何可移动类型
template <typename T>
void my_swap(T& a, T& b) {
T temp = std::move(a); // 移动而非拷贝
a = std::move(b);
b = std::move(temp);
}
// 为 std::string 提供特化版本(演示——实际无需,泛型版已足够)
template <>
void my_swap(std::string& a, std::string& b) {
std::cout << " [使用 std::string 特化版 swap]" << std::endl;
a.swap(b); // 更高效的成员 swap
}
// 泛型打印函数
template <typename T>
void print(const std::string& label, const T& value) {
std::cout << label << " = " << value << std::endl;
}
int main() {
std::cout << "===== 泛型 swap =====" << std::endl;
int x = 10, y = 20;
std::cout << "交换前: x=" << x << ", y=" << y << std::endl;
my_swap(x, y);
std::cout << "交换后: x=" << x << ", y=" << y << std::endl;
std::string s1 = "Hello", s2 = "World";
std::cout << "交换前: s1=" << s1 << ", s2=" << s2 << std::endl;
my_swap(s1, s2); // 调用特化版本
std::cout << "交换后: s1=" << s1 << ", s2=" << s2 << std::endl;
return 0;
}
预期输出:
===== 泛型 swap =====
交换前: x=10, y=20
交换后: x=20, y=10
交换前: s1=Hello, s2=World
[使用 std::string 特化版 swap]
交换后: s1=World, s2=Hello
易错场景与面试考点
易错场景
| 场景 | 错误表现 | 正确做法 |
|---|---|---|
类型推导歧义:max(1, 2.5) | 编译错误 | max<double>(1, 2.5) 或 max<int>(1, 2.5) |
数组传参退化:template<T> f(T arr) | T 推导为指针 | 用 T (&arr)[N] 非类型模板参数 |
返回类型推导:auto vs decltype(auto) | auto 丢弃引用和 cv | 需要保留引用/const 时用 decltype(auto) |
| 模板定义在 .cpp 文件中 | 链接错误 | 模板定义必须在头文件(或显式实例化) |
typename 和 class 混用引起歧义 | 历史习惯 | 选择一种风格统一使用 |
常见面试问题
函数模板的实例化时机?——模板被使用时(ODR-used),编译器根据实参类型生成具体函数代码。这是隐式实例化。也可以显式实例化:
template int max(int, int);。模板参数推导失败的情况有哪些?——(1) 同一个 T 推导出不同类型(
max(1, 2.5));(2) 模板参数只出现在无法推导的上下文中(如T::value_type);(3) 非类型模板参数无法从实参推导。什么是两阶段查找(Two-Phase Lookup)?——第一阶段在模板定义时查找非依赖名并做语法检查;第二阶段在模板实例化时查找依赖名并做语义检查。ADL 在第二阶段进行。
为什么模板函数通常定义在头文件中?——编译器需要看到完整的模板定义才能实例化。定义在 .cpp 中会导致其他翻译单元找不到定义。解决方案:头文件全定义、或使用
extern template+ 显式实例化。typename和class在模板参数中有区别吗?——没有。template<typename T>和template<class T>完全等价。typename更准确(T 不一定是类),但class更短。
小结
- 函数模板是编译期多态——为每种使用的类型生成一份代码
- 模板参数推导优先于隐式转换——类型不一致时显式指定参数
- 两阶段查找:非依赖名在定义时查,依赖名在实例化时查
- 模板定义必须在头文件——除非使用显式实例化
- 非类型模板参数(如
size_t N)在编译期确定,零运行时开销