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关键字:不只是语法糖
在模板参数列表中,typename和class可以互换。但有一个场景必须用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配合模板条件判断
- 策略模式:通过模板参数选择不同的算法实现
2.6 本章知识体系
下面这张图帮你梳理模板基础的核心脉络:
这张图把模板基础分成了三大块:函数模板、类模板、模板元编程。函数模板和类模板是日常开发的主力,模板元编程是进阶武器。而typename和class这两个关键字,贯穿所有模板场景,尤其是typename在依赖类型上的用法,一定要记牢。
好了,模板基础就聊到这儿。记住一个原则:模板是工具,不是炫技。能用简单代码解决的问题,别硬上模板。但当你需要写通用库、做编译期优化时,模板就是你最趁手的兵器。