模板基础:函数模板、类模板、模板特化、模板参数推导

模板这玩意儿,说白了就是C++里的「代码生成器」。你写一套逻辑,编译器帮你生成不同版本的代码。我刚开始学的时候觉得它很玄乎,后来在项目里写了一个通用的容器类,才真正体会到——没有模板,C++的泛型编程根本玩不转。

一句话总结:模板让你用同一份代码,处理不同类型的数据。编译时展开,运行时零开销。

1. 函数模板:让函数「百变」

先看一个最经典的例子——交换两个变量的值。如果没有模板,你得为int、double、string各写一个版本。有了模板,一行搞定:

template <typename T>
void mySwap(T& a, T& b) {
    T temp = a;
    a = b;
    b = temp;
}

调用的时候,编译器会根据实参自动推导T的类型:

int x = 1, y = 2;
mySwap(x, y);  // T 被推导为 int

double p = 3.14, q = 2.71;
mySwap(p, q);  // T 被推导为 double

我个人习惯把模板声明和定义都放在头文件里。为什么?因为模板是在编译期实例化的,链接器找不到单独的编译单元。嗯,这里要注意——模板不是普通函数,它不能先声明再放到.cpp里编译。

小技巧:如果你想让模板只对某些类型生效,可以用 static_assert 做类型检查。比如只允许数值类型:static_assert(std::is_arithmetic_v<T>);

2. 类模板:打造通用数据结构

类模板比函数模板更常用。我写过的一个简易栈容器,就是典型的类模板:

template <typename T, int MaxSize = 100>
class Stack {
private:
    T data[MaxSize];
    int top;
public:
    Stack() : top(-1) {}
    void push(const T& val) {
        if (top < MaxSize - 1) data[++top] = val;
    }
    T pop() {
        if (top >= 0) return data[top--];
        throw std::out_of_range("Stack empty");
    }
};

注意这里第二个模板参数是 int 类型,不是 typename。这叫非类型模板参数,必须是编译期常量。我在项目中遇到过一个问题——有人传了一个变量进去,编译器直接报错。你想想看,模板参数必须在编译期确定,变量是运行期的,当然不行。

使用起来很直观:

Stack<double, 50> doubleStack;  // 存double,容量50
Stack<std::string> strStack;    // 存string,默认容量100

3. 模板特化:给特殊类型开小灶

有时候,通用模板对某些类型不适用。比如你想写一个通用的比较函数,但遇到 const char* 时,直接比较指针地址显然不对,应该比较字符串内容。这时候就需要特化。

全特化:指定所有模板参数的具体类型。

template <typename T>
bool isEqual(const T& a, const T& b) {
    return a == b;
}

// 全特化版本:针对 const char*
template <>
bool isEqual<const char*>(const char* a, const char* b) {
    return strcmp(a, b) == 0;
}

偏特化:只指定部分模板参数。常用于类模板,函数模板不支持偏特化(但可以用重载模拟)。

template <typename T, typename U>
class Pair {
    // 通用版本
};

// 偏特化:两个类型相同
template <typename T>
class Pair<T, T> {
    // 优化版本,可以节省内存
};

我曾经踩过一个坑——给一个模板类写了偏特化,但忘记把通用版本的所有成员函数都实现一遍。结果链接时报了一堆未定义错误。所以特化时,一定要把接口实现完整。

避坑指南:模板特化必须在原模板的同一个命名空间中定义。我曾经把特化写到了另一个命名空间里,编译器死活找不到,排查了半天。

4. 模板参数推导:编译器替你「猜」类型

C++17 开始,类模板也能自动推导类型了。以前你必须写:

std::pair<int, double> p(1, 3.14);

现在可以简写为:

std::pair p(1, 3.14);  // 编译器自动推导为 pair<int, double>

函数模板的推导更早就有。但有几个细节要注意:

  • 引用折叠:当模板参数是 T&& 时,传入左值会推导为 T&,传入右值推导为 T。这是完美转发的核心机制。
  • 自动类型转换:模板推导不会做隐式类型转换。比如 template<typename T> void f(T a);,传 intf(double) 不会自动转,必须显式指定 f<double>(42)
  • decltype(auto):C++14 引入,用于推导返回类型,保留引用和const属性。

举个例子,你写一个返回两个数中较大者的模板:

template <typename T>
auto max(const T& a, const T& b) -> decltype(a > b ? a : b) {
    return a > b ? a : b;
}

这里用尾置返回类型,让编译器推导出正确的返回类型。C++14 之后可以直接写 decltype(auto),更简洁。

5. 知识体系总览

下面这张图帮你理清模板的核心脉络:

C++ 模板 函数模板 template<typename T> 自动推导参数类型 支持重载 类模板 template<typename T, int N> 非类型模板参数 成员函数模板 模板特化 全特化:所有参数指定 偏特化:部分参数指定 函数不支持偏特化 参数推导 C++17 类模板推导 引用折叠规则 decltype(auto) 实际应用 STL 容器(vector, map) 智能指针(shared_ptr) 泛型算法(sort, find)

6. 实战经验总结

最后分享几个我这些年积累的模板使用心得:

场景 推荐做法 常见坑
函数模板 用 typename 而非 class(语义更清晰) 模板定义放头文件,否则链接报错
类模板 非类型参数用 constexpr 或 enum 模拟 成员函数模板不能是虚函数
模板特化 优先用重载或 if constexpr 替代特化 特化必须在原命名空间
参数推导 C++17 后多用 auto 模板参数 推导不进行隐式类型转换
核心原则:模板是编译期的多态,不是运行期的。能用模板解决的问题,尽量别用虚函数。零开销抽象是C++的哲学。

模板这东西,刚开始觉得语法别扭,用多了就会发现它真的很强大。我建议你从写一个简单的 Array<T> 类开始练手,然后试着给它加迭代器、加特化。等你把模板玩熟了,再看STL源码,会有豁然开朗的感觉。


公众号:蓝海资料掘金营,微信deep3321