模板:C++泛型编程的基石
模板这东西,说白了就是「让编译器帮你写代码」。我刚开始学C++时,觉得模板特别玄乎,后来在Qt项目里写了一个通用数据缓存类,才真正体会到它的威力。今天咱们就把函数模板、类模板、模板特化,还有它们跟STL的关系,一次性讲透。
函数模板:一劳永逸的写法
假设你要写一个交换两个变量的函数。int版本写一个,double版本再写一个,string版本还得写一个……烦不烦?
函数模板就是来解决这个问题的。你写一次,编译器帮你生成所有版本。
template <typename T>
void mySwap(T& a, T& b) {
T temp = a;
a = b;
b = temp;
}
// 使用
int x = 3, y = 5;
mySwap(x, y); // T 被推导为 int
double d1 = 1.2, d2 = 3.4;
mySwap(d1, d2); // T 被推导为 double
注意那个 typename 关键字,也可以用 class,效果完全一样。我个人习惯用 typename,因为更直观——它就是个「类型名字」嘛。
mySwap<int>(x, y);。这在某些复杂场景下能避免歧义。
类模板:让类也「通用化」
函数模板搞定了函数,那类呢?比如你要写一个栈,既能存int,又能存double,还能存自定义类型。用类模板就对了。
template <typename T>
class Stack {
private:
std::vector<T> data;
public:
void push(const T& value) { data.push_back(value); }
T pop() {
T top = data.back();
data.pop_back();
return top;
}
bool isEmpty() const { return data.empty(); }
};
// 使用
Stack<int> intStack;
intStack.push(42);
Stack<std::string> strStack;
strStack.push("hello");
这里有个坑——类模板的成员函数定义,通常要写在头文件里。为什么?因为编译器在实例化时需要看到完整定义。我曾经把模板实现写在.cpp里,结果链接报了一堆未定义错误……嗯,从那以后我就记住了。
模板特化:给特殊情况开小灶
模板虽好,但不是所有类型都适用同一套逻辑。比如你写了一个比较大小的模板,对于普通类型用 > 没问题,但对于 const char* 字符串,直接比较指针地址就错了。
这时候就需要模板特化——为特定类型提供专门实现。
// 主模板
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后面跟一对尖括号,然后写函数名和具体类型。
偏特化:只针对部分参数特化
类模板还支持偏特化——只固定部分模板参数。比如你有一个接受两个类型的模板,想针对「两个类型相同」的情况做优化:
// 主模板
template <typename T1, typename T2>
class Pair {
// 通用实现
};
// 偏特化:当 T1 和 T2 相同时
template <typename T>
class Pair<T, T> {
// 优化实现
};
注意:函数模板不支持偏特化,只有类模板可以。如果你需要函数模板的「偏特化」效果,可以用重载或者SFINAE(这个后面再聊)。
模板与STL:天生一对
STL(标准模板库)几乎全是模板。你想想看:
std::vector<int>—— 类模板实例化std::sort(vec.begin(), vec.end())—— 函数模板std::unique_ptr<MyClass>—— 智能指针也是模板
没有模板,STL根本不可能存在。因为STL的设计哲学就是「算法与数据结构分离」,而模板正是实现这种分离的利器。
举个例子,std::sort 可以排序任何类型的容器,只要该类型支持随机访问迭代器:
std::vector<int> v = {3, 1, 4, 1, 5};
std::sort(v.begin(), v.end()); // 排序 int
std::vector<std::string> vs = {"apple", "banana", "cherry"};
std::sort(vs.begin(), vs.end()); // 排序 string
同一个算法,不同类型,一份代码。这就是模板的魔力。
operator< 的类型给 std::sort,编译器会吐出一大堆模板实例化堆栈。这时候别慌,从最底层的错误信息看起,通常能找到真正的问题。
知识体系总览
下面这张图帮你理清模板的核心脉络:
避坑指南
最后分享几个我踩过的坑:
- 模板代码的编译时错误:模板实例化发生在编译期,所以错误信息往往指向实例化点,而不是模板定义本身。调试时先确认模板参数类型是否正确。
- 特化与重载的混淆:函数模板特化不会参与重载决议。如果你写了特化版本,但调用时匹配了另一个重载,编译器不会报错,但行为可能不是你想要的。
- 模板代码膨胀:每个不同的模板参数都会生成一份独立代码。如果你在循环里用
int和long分别实例化同一个模板,代码体积会翻倍。对于嵌入式开发,这点要格外注意。
模板是C++里一个「入门容易精通难」的话题。今天讲的这些,足够你应付日常开发了。等你用熟了,自然会想去了解更高级的技巧——比如变参模板、SFINAE、概念(C++20)。但那是后话了。