模板基础回顾:函数模板、类模板、模板特化与偏特化、模板参数类型
各位同学,咱们今天聊聊模板的基础。说实话,这部分内容看起来简单,但我在面试中见过不少人栽在这里。你想想看,模板是C++元编程的基石,地基不稳,后面那些花哨的编译期计算根本玩不转。
我个人习惯把模板理解成「编译器帮我们写代码」的机制。你写一个模板,编译器根据实际使用情况,生成对应的具体代码。说白了,就是代码生成器,只不过它生在编译期。
函数模板:最朴素的泛型
函数模板是入门级的东西。我当年刚学C++时,觉得这玩意儿不就是Java的泛型吗?后来才发现,C++的模板远比Java泛型强大——它是在编译期做类型替换,而不是在运行时擦除类型。
template<typename T>
T max(T a, T b) {
return a > b ? a : b;
}
// 使用
int x = max(3, 5); // T 推导为 int
double y = max(3.14, 2.7); // T 推导为 double
嗯,这里要注意一个细节:模板参数推导。编译器会根据实参类型自动推导出T。但如果你写 max(3, 3.14),编译器会懵掉——T到底是int还是double?这时候就需要你显式指定:max<double>(3, 3.14)。
类模板:数据结构的骨架
类模板比函数模板更常用。你想想看,std::vector、std::map这些标准库容器,全是类模板。没有模板,我们得为每种类型手写一套容器——那画面太美我不敢看。
template<typename T>
class Stack {
private:
std::vector<T> elements;
public:
void push(const T& elem) { elements.push_back(elem); }
T pop() {
T top = elements.back();
elements.pop_back();
return top;
}
};
// 使用
Stack<int> intStack;
Stack<std::string> stringStack;
我在项目中遇到过一个问题:类模板的成员函数定义,如果写在类外面,必须带上模板声明。很多人忘了这个,编译报错半天找不到原因。
template<typename T>
void Stack<T>::push(const T& elem) {
elements.push_back(elem);
}
模板特化与偏特化:给特殊情况开小灶
模板特化,说白了就是「大部分情况用通用逻辑,但某些特殊类型我要单独处理」。比如你写了一个通用的排序模板,但遇到const char*时,你想用字符串比较而不是指针比较——这时候特化就派上用场了。
全特化:针对特定类型的定制
// 通用模板
template<typename T>
class TypeInfo {
public:
static std::string name() { return "unknown"; }
};
// 全特化:针对 int
template<>
class TypeInfo<int> {
public:
static std::string name() { return "int"; }
};
// 全特化:针对 double
template<>
class TypeInfo<double> {
public:
static std::string name() { return "double"; }
};
全特化的语法有点怪——template后面跟一对尖括号,里面什么都不写。我刚开始学的时候,总觉得这像是语法错误。但这就是C++,习惯就好。
偏特化:部分参数的定制
偏特化是C++模板最强大的特性之一。它允许你只特化部分模板参数,或者对某些模式进行特化。比如指针类型的特化:
// 通用模板
template<typename T>
class IsPointer {
public:
static constexpr bool value = false;
};
// 偏特化:T* 类型
template<typename T>
class IsPointer<T*> {
public:
static constexpr bool value = true;
};
// 使用
std::cout << IsPointer<int>::value; // 0
std::cout << IsPointer<int*>::value; // 1
为什么会这样?因为偏特化匹配的是模式。当你传入 int* 时,编译器发现它匹配 T* 这个模式,于是T被推导为int,然后选择偏特化版本。
模板参数类型:不只是typename
很多人以为模板参数只能是类型,其实不然。模板参数有四种:
| 参数类型 | 示例 | 说明 |
|---|---|---|
| 类型参数 | typename T / class T | 最常见的,代表一个类型 |
| 非类型参数 | int N / std::size_t N | 编译期常量,如数组大小 |
| 模板模板参数 | template<typename> class Container | 参数本身是一个模板 |
| 可变参数模板 | typename... Args | 接受任意数量的类型参数 |
非类型参数我特别想多说两句。我在项目中用它来实现编译期定长的数组:
template<typename T, std::size_t N>
class FixedArray {
T data[N];
public:
constexpr std::size_t size() const { return N; }
T& operator[](std::size_t i) { return data[i]; }
};
FixedArray<int, 10> arr; // 编译期就知道大小
嗯,这里要注意:非类型参数必须是编译期常量。你不能传一个变量进去,只能传字面量、constexpr变量、或者枚举值。
模板模板参数稍微复杂点,但非常有用。比如你想写一个容器适配器,不绑定具体容器类型:
template<typename T, template<typename> class Container = std::vector>
class MyContainer {
Container<T> data;
// ...
};
MyContainer<int> c1; // 默认用 vector
MyContainer<int, std::deque> c2; // 用 deque
可变参数模板是C++11引入的,它让模板可以接受任意数量的参数。这个在后面的章节会大量用到,今天先留个印象:
template<typename... Args>
void printAll(Args... args) {
(std::cout << ... << args) << std::endl; // C++17 折叠表达式
}
printAll(1, 2.5, "hello", 'c'); // 输出:12.5helloc
知识体系总览
下面这张图,是我对模板基础部分的理解框架。你可以把它当作一个思维导图:
这张图把今天讲的内容串起来了。你仔细看,四个分支其实对应了模板的四个维度:用途(函数 vs 类)、定制能力(特化)、参数灵活性(多种参数类型)。掌握了这些,你就有了玩转模板元编程的基础。
好了,今天就到这儿。记住一句话:模板不是魔法,它只是编译器替你写代码的一种方式。理解了这个本质,后面那些花哨的编译期计算,你自然就能看懂了。