函数模板

原理

模板实例化机制

编译器看到模板定义时不生成任何代码——仅进行第一阶段语法检查(括号匹配等与模板参数无关的检查)。只有当模板被调用(实例化)时,编译器才根据实际模板实参生成具体函数——此即第二阶段检查(实例化时检查)。

这个两阶段编译模型的核心:

  • 第一阶段:检查非依赖名(不依赖模板参数的代码)
  • 第二阶段:实例化时检查依赖名(依赖模板参数的代码)
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 等类型萃取的实际应用场景。