2、C++模板基础:函数模板、类模板、模板特化、模板元编程简介、typename与class关键字

模板这东西,说白了就是C++里的「代码生成器」。你写一次逻辑,编译器帮你生成多种类型的版本。我刚学模板时觉得它就是个高级宏,后来才发现——它其实是C++静态多态和泛型编程的基石。今天咱们就把模板的基础掰开揉碎,从函数模板一直聊到模板元编程的门口。

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

先看个最经典的场景:你要写一个交换两个变量的函数。如果只支持int,那double怎么办?string怎么办?

// 传统写法:每个类型写一遍
void swap_int(int& a, int& b) { int t = a; a = b; b = t; }
void swap_double(double& a, double& b) { double t = a; a = b; b = t; }
// ... 无穷无尽

用模板就清爽多了:

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

调用时,编译器会根据实参自动推导T的类型。你写my_swap(x, y),如果x和y是int,T就是int;如果是double,T就是double。我在项目中经常用这个模式来写通用的排序工具函数,省掉大量重复代码。

小技巧: 函数模板的模板参数也可以显式指定,比如 my_swap<int>(a, b)。但大多数情况下让编译器自己推导更省事。

2.2 类模板:把类型变成参数

函数模板处理的是「一个函数适配多种类型」,类模板则是「一个类适配多种类型」。最典型的例子就是STL里的vector、list这些容器。

template <typename T>
class Box {
public:
    Box(T val) : value(val) {}
    T getValue() const { return value; }
    void setValue(T val) { value = val; }
private:
    T value;
};

// 使用
Box<int> intBox(42);
Box<std::string> strBox("Hello");

类模板的成员函数定义如果写在类外,语法稍微啰嗦一点:

template <typename T>
void Box<T>::setValue(T val) {
    value = val;
}

嗯,这里要注意:类模板的每个成员函数前面都要加 template <typename T>,并且类名后面要带 <T>。我刚开始写的时候经常漏掉这个,编译报错半天找不出原因。

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

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

2.3.1 全特化

// 通用模板
template <typename T>
bool compare(const T& a, const T& b) {
    return a == b;
}

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

全特化的语法就是 template <> 后面跟具体的函数签名。我曾经在写一个序列化库时,需要对std::vector做特殊处理——普通类型直接memcpy,但vector需要逐个元素序列化。全特化帮我完美解决了这个问题。

2.3.2 偏特化

偏特化只适用于类模板,函数模板不支持。它允许你只特化部分模板参数,或者对指针类型做特殊处理。

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

// 偏特化:两个类型相同时
template <typename T>
class Pair<T, T> {
    // 优化实现
};

// 偏特化:第一个参数是指针时
template <typename T, typename U>
class Pair<T*, U> {
    // 指针版本
};
避坑指南: 我曾经在项目里写了一个偏特化版本,结果编译器一直匹配不到。后来发现是偏特化的参数列表和通用模板的参数个数不一致。记住:偏特化不能改变模板参数的数量,只能改变类型约束。

2.4 typename与class关键字:不只是语法糖

在模板参数列表中,typenameclass可以互换。但有一个场景必须用typename——当你要引用一个「依赖类型」时。

template <typename T>
void printSize(const T& container) {
    // 这里必须用 typename,因为 T::size_type 是依赖类型
    typename T::size_type size = container.size();
    std::cout << size << std::endl;
}

为什么?因为编译器在解析模板时,还不知道T是什么。如果不加typename,编译器会把T::size_type当成一个静态成员变量,而不是类型。我见过不少新手在这里栽跟头,编译报错「expected ';' before 'size'」,其实就是忘了加typename

记住这个规则: 在模板内部,如果某个名字依赖于模板参数,并且你想把它当作类型使用,前面必须加 typename

2.5 模板元编程简介:让编译器做计算

模板元编程(TMP)是C++里最「黑魔法」的部分。它的核心思想是:利用模板的编译期实例化机制,在编译阶段完成计算。说白了,就是把运行时的工作挪到编译期。

看一个最经典的例子——编译期阶乘:

template <int N>
struct Factorial {
    static const int value = N * Factorial<N - 1>::value;
};

template <>
struct Factorial<0> {
    static const int value = 1;
};

// 使用
int main() {
    std::cout << Factorial<5>::value;  // 输出 120,编译期就算好了
}

你想想看,这个Factorial<5>::value在编译时就被展开成5 * 4 * 3 * 2 * 1,最终变成一个常量。运行时没有任何计算开销。

模板元编程的典型应用场景:

  • 类型萃取(type traits):判断一个类型是否是整数、是否是指针等
  • 编译期断言:static_assert配合模板条件判断
  • 策略模式:通过模板参数选择不同的算法实现
我的建议: 模板元编程虽然强大,但不要滥用。代码可读性会急剧下降。我在项目中只在性能关键路径上使用TMP,比如网络库的缓冲区管理、序列化框架的编译期优化。日常业务代码,老老实实用普通函数和类就好。

2.6 本章知识体系

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

C++ 模板基础 函数模板 自动类型推导 显式指定模板参数 重载与特化 类模板 全特化:完全指定类型 偏特化:部分约束类型 成员函数类外定义 模板元编程:编译期计算 核心:typename 用于依赖类型,class 与 typename 在参数列表中可互换

这张图把模板基础分成了三大块:函数模板、类模板、模板元编程。函数模板和类模板是日常开发的主力,模板元编程是进阶武器。而typenameclass这两个关键字,贯穿所有模板场景,尤其是typename在依赖类型上的用法,一定要记牢。

好了,模板基础就聊到这儿。记住一个原则:模板是工具,不是炫技。能用简单代码解决的问题,别硬上模板。但当你需要写通用库、做编译期优化时,模板就是你最趁手的兵器。


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