模板基础回顾:函数模板、类模板、模板特化与偏特化、模板参数类型

各位同学,咱们今天聊聊模板的基础。说实话,这部分内容看起来简单,但我在面试中见过不少人栽在这里。你想想看,模板是C++元编程的基石,地基不稳,后面那些花哨的编译期计算根本玩不转。

我个人习惯把模板理解成「编译器帮我们写代码」的机制。你写一个模板,编译器根据实际使用情况,生成对应的具体代码。说白了,就是代码生成器,只不过它生在编译期。

函数模板:最朴素的泛型

函数模板是入门级的东西。我当年刚学C++时,觉得这玩意儿不就是Java的泛型吗?后来才发现,C++的模板远比Java泛型强大——它是在编译期做类型替换,而不是在运行时擦除类型。

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

// 使用
int x = max(3, 5);        // T 推导为 int
double y = max(3.14, 2.7); // T 推导为 double

嗯,这里要注意一个细节:模板参数推导。编译器会根据实参类型自动推导出T。但如果你写 max(3, 3.14),编译器会懵掉——T到底是int还是double?这时候就需要你显式指定:max<double>(3, 3.14)

小技巧:我建议在写函数模板时,如果参数类型可能不一致,要么用多个模板参数,要么让调用者显式指定。别指望编译器替你猜,它没那么聪明。

类模板:数据结构的骨架

类模板比函数模板更常用。你想想看,std::vector、std::map这些标准库容器,全是类模板。没有模板,我们得为每种类型手写一套容器——那画面太美我不敢看。

template<typename T>
class Stack {
private:
    std::vector<T> elements;
public:
    void push(const T& elem) { elements.push_back(elem); }
    T pop() {
        T top = elements.back();
        elements.pop_back();
        return top;
    }
};

// 使用
Stack<int> intStack;
Stack<std::string> stringStack;

我在项目中遇到过一个问题:类模板的成员函数定义,如果写在类外面,必须带上模板声明。很多人忘了这个,编译报错半天找不到原因。

template<typename T>
void Stack<T>::push(const T& elem) {
    elements.push_back(elem);
}
注意:类模板的模板参数,在类外定义成员函数时,必须重复写 template<typename T>。我曾经见过一个同事,写了整整一页的类模板,结果所有成员函数定义都忘了加模板声明——编译错误刷了满屏。

模板特化与偏特化:给特殊情况开小灶

模板特化,说白了就是「大部分情况用通用逻辑,但某些特殊类型我要单独处理」。比如你写了一个通用的排序模板,但遇到const char*时,你想用字符串比较而不是指针比较——这时候特化就派上用场了。

全特化:针对特定类型的定制

// 通用模板
template<typename T>
class TypeInfo {
public:
    static std::string name() { return "unknown"; }
};

// 全特化:针对 int
template<>
class TypeInfo<int> {
public:
    static std::string name() { return "int"; }
};

// 全特化:针对 double
template<>
class TypeInfo<double> {
public:
    static std::string name() { return "double"; }
};

全特化的语法有点怪——template后面跟一对尖括号,里面什么都不写。我刚开始学的时候,总觉得这像是语法错误。但这就是C++,习惯就好。

偏特化:部分参数的定制

偏特化是C++模板最强大的特性之一。它允许你只特化部分模板参数,或者对某些模式进行特化。比如指针类型的特化:

// 通用模板
template<typename T>
class IsPointer {
public:
    static constexpr bool value = false;
};

// 偏特化:T* 类型
template<typename T>
class IsPointer<T*> {
public:
    static constexpr bool value = true;
};

// 使用
std::cout << IsPointer<int>::value;   // 0
std::cout << IsPointer<int*>::value;  // 1

为什么会这样?因为偏特化匹配的是模式。当你传入 int* 时,编译器发现它匹配 T* 这个模式,于是T被推导为int,然后选择偏特化版本。

核心要点:偏特化不是针对某个具体类型,而是针对一类类型模式。比如 T*、const T、std::vector<T> 等等。这是模板元编程的基础操作。

模板参数类型:不只是typename

很多人以为模板参数只能是类型,其实不然。模板参数有四种:

参数类型 示例 说明
类型参数 typename T / class T 最常见的,代表一个类型
非类型参数 int N / std::size_t N 编译期常量,如数组大小
模板模板参数 template<typename> class Container 参数本身是一个模板
可变参数模板 typename... Args 接受任意数量的类型参数

非类型参数我特别想多说两句。我在项目中用它来实现编译期定长的数组:

template<typename T, std::size_t N>
class FixedArray {
    T data[N];
public:
    constexpr std::size_t size() const { return N; }
    T& operator[](std::size_t i) { return data[i]; }
};

FixedArray<int, 10> arr;  // 编译期就知道大小

嗯,这里要注意:非类型参数必须是编译期常量。你不能传一个变量进去,只能传字面量、constexpr变量、或者枚举值。

模板模板参数稍微复杂点,但非常有用。比如你想写一个容器适配器,不绑定具体容器类型:

template<typename T, template<typename> class Container = std::vector>
class MyContainer {
    Container<T> data;
    // ...
};

MyContainer<int> c1;                    // 默认用 vector
MyContainer<int, std::deque> c2;        // 用 deque

可变参数模板是C++11引入的,它让模板可以接受任意数量的参数。这个在后面的章节会大量用到,今天先留个印象:

template<typename... Args>
void printAll(Args... args) {
    (std::cout << ... << args) << std::endl;  // C++17 折叠表达式
}

printAll(1, 2.5, "hello", 'c');  // 输出:12.5helloc
避坑指南:我曾经在模板模板参数上栽过跟头——标准库容器其实都有默认的分配器参数,比如 std::vector<T, Allocator>。所以直接写 template<typename> class Container 匹配不了 std::vector。正确的做法是用可变参数模板:template<typename...> class Container。

知识体系总览

下面这张图,是我对模板基础部分的理解框架。你可以把它当作一个思维导图:

C++ 模板基础 函数模板 类型推导、显式指定 类模板 成员函数、友元、静态成员 特化与偏特化 全特化、偏特化、模式匹配 模板参数类型 类型参数 (typename/class) 非类型参数 (int, size_t) 模板模板参数 可变参数模板 (...) 模板是编译期代码生成的核心机制

这张图把今天讲的内容串起来了。你仔细看,四个分支其实对应了模板的四个维度:用途(函数 vs 类)、定制能力(特化)、参数灵活性(多种参数类型)。掌握了这些,你就有了玩转模板元编程的基础。

好了,今天就到这儿。记住一句话:模板不是魔法,它只是编译器替你写代码的一种方式。理解了这个本质,后面那些花哨的编译期计算,你自然就能看懂了。


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