一、模板的起点:函数模板与类模板

模板这东西,说白了就是「让编译器帮你写代码」。我刚开始学C++那会儿,觉得模板就是个高级点的宏。后来被坑了几次才明白——模板是类型安全的,而且它在编译期就完成了所有工作。

今天咱们聊聊模板最基础的两个形态:函数模板和类模板。包括它们怎么定义、怎么实例化,还有特化和偏特化这些进阶玩法。

1.1 函数模板:让一个函数处理多种类型

先看个最简单的例子。假设你要写一个求最大值的函数:

template<typename T>
T max(T a, T b) {
    return a > b ? a : b;
}

这里的 typename T 就是模板参数。你可以把它理解成一个「类型占位符」。编译器看到这个模板,并不会生成任何代码——它只是记下了这个「配方」。

直到你真正调用它:

int x = max(3, 5);        // T 被推导为 int
double y = max(3.14, 2.71); // T 被推导为 double

这时候编译器才会为 intdouble 分别生成一份函数代码。这个过程就叫「实例化」。

我的习惯:函数模板的参数推导虽然方便,但有时候会推导出你不想要的结果。比如传一个 int 和一个 double,编译器会报错——因为两个参数类型不一致。这时候我通常会显式指定模板参数:max<double>(3, 5.2)

1.2 类模板:把类型也变成参数

函数模板处理的是「算法」,类模板处理的是「数据结构」。最经典的例子就是 std::vector

template<typename T>
class MyVector {
public:
    void push_back(const T& val);
    T& operator[](size_t index);
private:
    T* data;
    size_t size;
    size_t capacity;
};

类模板的实例化必须显式指定类型参数:

MyVector<int> vec;      // 实例化出 int 版本的类
MyVector<std::string> sv; // 实例化出 string 版本的类

注意,类模板的成员函数只有在被调用时才会实例化。这个特性很重要——它意味着你可以写出「部分可用」的模板代码。

我曾经踩过的坑:类模板的成员函数定义如果写在 .cpp 文件里,链接时会报「未定义符号」的错误。因为编译器在编译 .cpp 时看不到具体的类型参数,没法生成代码。解决方案很简单——把实现写在头文件里,或者用 .tpp 文件显式实例化。

1.3 模板特化:给特定类型开小灶

有时候,通用模板不能满足所有类型的需求。比如你要写一个类型打印工具:

template<typename T>
void print(const T& val) {
    std::cout << val << std::endl;
}

这个对 intdouble 都工作得很好。但如果 T 是 std::vector<int>,直接打印就不行了。这时候就需要特化:

template<>
void print<std::vector<int>>(const std::vector<int>& val) {
    for (auto v : val) {
        std::cout << v << " ";
    }
    std::cout << std::endl;
}

特化的语法有点怪——template<> 表示「这是一个特化版本」,后面跟具体的类型参数。编译器在匹配时,会优先选择特化版本。

1.4 偏特化:只特化部分参数

偏特化是类模板独有的能力。函数模板不支持偏特化(但可以用重载达到类似效果)。

举个例子,假设你有一个模板类:

template<typename T, typename U>
class Pair {
    // 通用实现
};

你想对「两个类型相同」的情况做特殊处理:

template<typename T>
class Pair<T, T> {
    // 偏特化版本:两个参数类型相同
};

也可以对「第二个参数是指针」的情况做特化:

template<typename T, typename U>
class Pair<T, U*> {
    // 偏特化版本:U 是指针
};

偏特化的匹配规则有点复杂。简单说:编译器会找「最特化」的那个版本。如果找不到合适的,就用通用模板。

核心要点:

  • 全特化:所有模板参数都指定具体类型
  • 偏特化:部分模板参数指定具体类型或约束
  • 函数模板只有全特化,没有偏特化
  • 类模板既有全特化,也有偏特化

1.5 知识体系总览

下面这张图把模板基础的核心脉络梳理清楚了。你看一眼就能明白各个概念之间的关系:

模板基础:函数模板与类模板 C++ 模板 函数模板 类模板 隐式实例化 显式实例化 全特化 隐式实例化 显式实例化 全特化 偏特化(仅类模板) 函数模板不支持偏特化,但可以通过重载达到类似效果 主模板 实例化/特化 偏特化

1.6 实战中的选择

我个人的经验是:能用函数模板就别用类模板,除非你需要保存状态或者定义多个相关类型。函数模板的推导机制更自然,写起来也更简洁。

但如果你要写一个容器、一个智能指针、或者一个策略类——那就必须用类模板了。这时候特化和偏特化就派上用场了。

举个例子,我在项目中写过一个内存池分配器:

// 通用分配器
template<typename T>
class Allocator {
    T* allocate(size_t n) { return new T[n]; }
    void deallocate(T* p) { delete[] p; }
};

// 偏特化:void 类型不需要分配
template<>
class Allocator<void> {
    // 空实现,或者直接禁止使用
};

这种设计在泛型编程中很常见。你提供一个通用版本,然后对特殊类型做针对性处理。

一个小技巧:如果你不确定该用特化还是重载,记住一条原则——特化是针对「类型」的,重载是针对「参数个数或类型组合」的。函数模板的重载比特化更灵活,也更安全。我建议优先用重载,除非你确实需要全特化。

好了,模板基础就聊到这儿。这些概念虽然基础,但它们是后面所有模板元编程的基石。你把这些搞清楚了,后面学 SFINAE、变参模板、类型萃取这些高级特性就会轻松很多。


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