函数模板
原理
模板实例化机制
编译器看到模板定义时不生成任何代码——仅进行第一阶段语法检查(括号匹配等与模板参数无关的检查)。只有当模板被调用(实例化)时,编译器才根据实际模板实参生成具体函数——此即第二阶段检查(实例化时检查)。
这个两阶段编译模型的核心:
- 第一阶段:检查非依赖名(不依赖模板参数的代码)
- 第二阶段:实例化时检查依赖名(依赖模板参数的代码)
template<typename T>
void foo(T x) {
x.hello(); // 第二阶段检查(依赖名)
int y = "abc"; // 第一阶段就报错(非依赖名)
}编译期多态 vs 运行时多态
| 模板(编译期多态) | 虚函数(运行时多态) | |
|---|---|---|
| 绑定时机 | 编译期 | 运行时 |
| 实现机制 | 代码生成(实例化) | vtable 间接调用 |
| 性能 | 零运行时开销,可内联 | 额外解引用,不可内联 |
| 代码体积 | 每种实例化一份代码(代码膨胀) | 一份函数体 |
| 灵活性 | 必须编译期确定类型 | 可运行时扩展 |
代码膨胀与去重
每种不同的模板参数组合产生一份独立的代码副本。同一个模板函数在多个编译单元中被实例化时,每个 .o 文件中都有一份机器码——链接器通过 COMDAT 折叠去重,最终只保留一份。
缓解策略:瘦模板技术(将不依赖模板参数的代码提取到非模板基类中);类型擦除(用 void* 只实例化一次);C++20 Concepts 约束实例化范围。
if constexpr 的编译期分支(C++17)
if constexpr 的条件在编译期求值,不满足条件的分支完全不被实例化——甚至连语法检查都不需要:
template<typename T>
auto format(const T& v) {
if constexpr (std::is_integral_v<T>)
return std::to_string(v); // 仅当 T 是整数时实例化
else if constexpr (std::is_same_v<T, std::string>)
return v; // 仅当 T 是 string 时实例化
}SFINAE 与 enable_if
SFINAE(Substitution Failure Is Not An Error)——模板参数替换失败时编译器不会报错,而是静默地跳过该候选函数。std::enable_if 利用此机制实现条件编译:
template<typename T>
typename std::enable_if<std::is_integral_v<T>, T>::type
safeDivide(T a, T b) { /* 整数版本 */ }
template<typename T>
typename std::enable_if<std::is_floating_point_v<T>, T>::type
safeDivide(T a, T b) { /* 浮点版本 */ }可变参数模板的编译期递归展开
可变参数模板通过编译期递归展开:
template<typename T>
void print(T v) { cout << v << endl; } // 终止条件
template<typename T, typename... Args>
void print(T first, Args... rest) {
cout << first << ", ";
print(rest...); // 每次递归参数包减少一个
}C++17 的折叠表达式简化了常用展开模式:
template<typename... Args>
auto sum(Args... args) { return (... + args); } // 一元左折叠语法
函数模板
template<typename T>
T max(T a, T b) { return a > b ? a : b; }
// 隐式实例化
max(3, 7); // T = int
max(3.14, 2.71); // T = double
// 显式实例化
max<int>(5.5, 3.3); // T = int,浮点数截断类模板
template<typename T>
class Stack {
T* data; int top, cap;
public:
void push(const T& v);
T pop();
};
Stack<int> s; // 实例化非类型模板参数
template<typename T, int N>
class Array {
T data[N];
};
Array<int, 5> arr; // N 是编译期常量模板特化:全特化与偏特化
// 通用模板
template<typename T> struct TypeInfo { static const char* name() { return "?"; } };
// 全特化
template<> struct TypeInfo<int> { static const char* name() { return "int"; } };
// 偏特化(仅针对指针类型)
template<typename T> struct TypeInfo<T*> {
static const char* name() { return "ptr"; }
};默认模板参数
template<typename T, typename Container = std::vector<T>>
class Queue { Container data; };decltype 与 auto 返回类型
template<typename T1, typename T2>
auto add(T1 a, T2 b) -> decltype(a + b) { return a + b; }
// C++14:直接 auto 推导
template<typename T1, typename T2>
auto multiply(T1 a, T2 b) { return a * b; }模板元编程
template<unsigned N>
struct Factorial {
static constexpr auto value = N * Factorial<N - 1>::value;
};
template<> struct Factorial<0> { static constexpr auto value = 1; };
// Factorial<5>::value → 120(编译期计算)实践
力扣题目:力扣第k小(理解 nth_element 的模板设计),力扣排序题(模板版本的 sort 实现)。
AI 自检:1) 用 if constexpr 写出一个函数,接受任意类型参数,若为整数输出平方、若为 string 输出反转;2) 问 AI 模板实例化后的代码在不同编译单元中如何避免重复——要求回答 COMDAT/ICF 机制。
建议先阅读:02_预处理器 — 宏的文本替换与模板的编译期代码生成之间的根本区别;03_指针与引用 — std::remove_reference_t 等类型萃取的实际应用场景。