一、模板的起点:函数模板与类模板
模板这东西,说白了就是「让编译器帮你写代码」。我刚开始学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
这时候编译器才会为 int 和 double 分别生成一份函数代码。这个过程就叫「实例化」。
我的习惯:函数模板的参数推导虽然方便,但有时候会推导出你不想要的结果。比如传一个 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;
}
这个对 int、double 都工作得很好。但如果 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 知识体系总览
下面这张图把模板基础的核心脉络梳理清楚了。你看一眼就能明白各个概念之间的关系:
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