模板基础:函数模板、类模板、模板特化、模板参数推导
模板这玩意儿,说白了就是C++里的「代码生成器」。你写一套逻辑,编译器帮你生成不同版本的代码。我刚开始学的时候觉得它很玄乎,后来在项目里写了一个通用的容器类,才真正体会到——没有模板,C++的泛型编程根本玩不转。
1. 函数模板:让函数「百变」
先看一个最经典的例子——交换两个变量的值。如果没有模板,你得为int、double、string各写一个版本。有了模板,一行搞定:
template <typename T>
void mySwap(T& a, T& b) {
T temp = a;
a = b;
b = temp;
}
调用的时候,编译器会根据实参自动推导T的类型:
int x = 1, y = 2;
mySwap(x, y); // T 被推导为 int
double p = 3.14, q = 2.71;
mySwap(p, q); // T 被推导为 double
我个人习惯把模板声明和定义都放在头文件里。为什么?因为模板是在编译期实例化的,链接器找不到单独的编译单元。嗯,这里要注意——模板不是普通函数,它不能先声明再放到.cpp里编译。
static_assert(std::is_arithmetic_v<T>);
2. 类模板:打造通用数据结构
类模板比函数模板更常用。我写过的一个简易栈容器,就是典型的类模板:
template <typename T, int MaxSize = 100>
class Stack {
private:
T data[MaxSize];
int top;
public:
Stack() : top(-1) {}
void push(const T& val) {
if (top < MaxSize - 1) data[++top] = val;
}
T pop() {
if (top >= 0) return data[top--];
throw std::out_of_range("Stack empty");
}
};
注意这里第二个模板参数是 int 类型,不是 typename。这叫非类型模板参数,必须是编译期常量。我在项目中遇到过一个问题——有人传了一个变量进去,编译器直接报错。你想想看,模板参数必须在编译期确定,变量是运行期的,当然不行。
使用起来很直观:
Stack<double, 50> doubleStack; // 存double,容量50
Stack<std::string> strStack; // 存string,默认容量100
3. 模板特化:给特殊类型开小灶
有时候,通用模板对某些类型不适用。比如你想写一个通用的比较函数,但遇到 const char* 时,直接比较指针地址显然不对,应该比较字符串内容。这时候就需要特化。
全特化:指定所有模板参数的具体类型。
template <typename T>
bool isEqual(const T& a, const T& b) {
return a == b;
}
// 全特化版本:针对 const char*
template <>
bool isEqual<const char*>(const char* a, const char* b) {
return strcmp(a, b) == 0;
}
偏特化:只指定部分模板参数。常用于类模板,函数模板不支持偏特化(但可以用重载模拟)。
template <typename T, typename U>
class Pair {
// 通用版本
};
// 偏特化:两个类型相同
template <typename T>
class Pair<T, T> {
// 优化版本,可以节省内存
};
我曾经踩过一个坑——给一个模板类写了偏特化,但忘记把通用版本的所有成员函数都实现一遍。结果链接时报了一堆未定义错误。所以特化时,一定要把接口实现完整。
4. 模板参数推导:编译器替你「猜」类型
C++17 开始,类模板也能自动推导类型了。以前你必须写:
std::pair<int, double> p(1, 3.14);
现在可以简写为:
std::pair p(1, 3.14); // 编译器自动推导为 pair<int, double>
函数模板的推导更早就有。但有几个细节要注意:
- 引用折叠:当模板参数是
T&&时,传入左值会推导为T&,传入右值推导为T。这是完美转发的核心机制。 - 自动类型转换:模板推导不会做隐式类型转换。比如
template<typename T> void f(T a);,传int给f(double)不会自动转,必须显式指定f<double>(42)。 - decltype(auto):C++14 引入,用于推导返回类型,保留引用和const属性。
举个例子,你写一个返回两个数中较大者的模板:
template <typename T>
auto max(const T& a, const T& b) -> decltype(a > b ? a : b) {
return a > b ? a : b;
}
这里用尾置返回类型,让编译器推导出正确的返回类型。C++14 之后可以直接写 decltype(auto),更简洁。
5. 知识体系总览
下面这张图帮你理清模板的核心脉络:
6. 实战经验总结
最后分享几个我这些年积累的模板使用心得:
| 场景 | 推荐做法 | 常见坑 |
|---|---|---|
| 函数模板 | 用 typename 而非 class(语义更清晰) | 模板定义放头文件,否则链接报错 |
| 类模板 | 非类型参数用 constexpr 或 enum 模拟 | 成员函数模板不能是虚函数 |
| 模板特化 | 优先用重载或 if constexpr 替代特化 | 特化必须在原命名空间 |
| 参数推导 | C++17 后多用 auto 模板参数 | 推导不进行隐式类型转换 |
模板这东西,刚开始觉得语法别扭,用多了就会发现它真的很强大。我建议你从写一个简单的 Array<T> 类开始练手,然后试着给它加迭代器、加特化。等你把模板玩熟了,再看STL源码,会有豁然开朗的感觉。
公众号:蓝海资料掘金营,微信deep3321