模板参数详解:类型参数、非类型参数、模板模板参数
模板参数这个话题,说实话,很多人学了很久 C++ 都只用了最基础的那一层。我见过不少项目,模板参数清一色全是 typename T,然后就没有然后了。其实模板参数远不止这么简单——类型参数、非类型参数、模板模板参数,这三者构成了 C++ 泛型编程的完整骨架。
今天我就带你把这三种参数彻底捋清楚。嗯,咱们一个一个来。
1. 类型参数:最熟悉的陌生人
类型参数,说白了就是用 typename 或 class 声明的参数。它代表一个类型,可以是 int、string、自定义类,甚至是另一个模板。
template <typename T>
class Wrapper {
T value_;
public:
explicit Wrapper(T v) : value_(v) {}
T get() const { return value_; }
};
这里 T 就是类型参数。你传什么类型进去,Wrapper 就变成什么类型的容器。
关键点: typename 和 class 在声明类型参数时完全等价。我个人习惯用 typename,因为语义更明确——你就是在声明一个「类型的名字」。
但有个坑要注意:在模板内部访问嵌套类型时,必须用 typename 关键字。比如:
template <typename T>
void foo() {
// 如果 T::iterator 是一个类型,必须加 typename
typename T::iterator it;
}
我曾经在一个代码评审里看到有人漏掉了这个 typename,编译报错找了半小时。嗯,编译器其实很严格,它分不清 T::iterator 是类型还是静态成员,你得帮它指明。
2. 非类型参数:模板里的「常量」
非类型参数,就是模板参数里可以放一个具体的值,而不是类型。比如整数、枚举、指针、引用等。
template <typename T, size_t N>
class Array {
T data_[N];
public:
size_t size() const { return N; }
T& operator[](size_t i) { return data_[i]; }
};
这里 N 就是非类型参数,它是一个 size_t 类型的常量。你想想看,这比用 std::vector 轻量多了——栈上分配,没有堆开销。
小技巧: 非类型参数可以是 int、char、bool、enum,甚至 C++17 之后可以是 auto。但浮点数和字符串字面量不行——这是标准规定的,别问我为什么,我也觉得有点遗憾。
使用方式也很直观:
Array<int, 10> arr; // 10 个 int 的数组
arr[0] = 42;
std::cout << arr.size(); // 输出 10
非类型参数最常见的场景就是编译期常量。我在项目中用它来实现固定大小的缓冲区,避免了动态分配,性能提升很明显。
注意: 非类型参数必须是编译期可求值的常量表达式。你不能传一个变量进去,比如 int n = 5; Array<int, n> arr; 会编译失败。除非你把 n 声明为 constexpr。
3. 模板模板参数:模板的模板
这个就有点意思了。模板模板参数,顾名思义——模板参数本身也是一个模板。
举个例子,你想写一个容器适配器,它接受一个容器类型作为参数,但这个容器本身也是模板(比如 std::vector、std::list)。这时候就需要模板模板参数。
template <typename T, template <typename> class Container>
class Adapter {
Container<T> container_;
public:
void push(const T& val) { container_.push_back(val); }
// ...
};
注意这里的语法:template <typename> class Container——它声明了一个模板参数 Container,这个参数本身是一个模板,接受一个类型参数。
使用方式:
Adapter<int, std::vector> vecAdapter;
Adapter<double, std::list> listAdapter;
是不是很灵活?你想想看,如果没有模板模板参数,你可能得写两个不同的适配器类,或者用 std::function 绕一圈。但有了它,一个模板搞定所有容器。
核心理解: 模板模板参数让代码的抽象层次又高了一层。你不再只是「对类型编程」,而是「对模板编程」。说白了,就是元编程的入门砖。
不过这里有个坑:标准库容器通常有多个模板参数(比如 std::vector 有分配器参数),所以直接传 std::vector 可能匹配不上。解决办法是用别名模板包装一下:
template <typename T>
using MyVector = std::vector<T, std::allocator<T>>;
Adapter<int, MyVector> adapter;
我曾经在重构一个老项目时,发现有人用宏来实现类似功能,代码又丑又难维护。换成模板模板参数后,整个设计清爽多了。
4. 三种参数对比一览
| 参数类型 | 关键字/语法 | 示例 | 典型用途 |
|---|---|---|---|
| 类型参数 | typename / class |
template<typename T> |
泛型容器、算法 |
| 非类型参数 | int、size_t 等 |
template<typename T, size_t N> |
编译期常量、数组大小 |
| 模板模板参数 | template<typename> class |
template<typename T, template<typename> class C> |
容器适配器、策略模式 |
5. 知识体系结构图
下面这张图帮你理清三种参数的关系和适用场景:
6. 实战中的选择建议
这三种参数怎么选?我个人的经验是:
- 类型参数:90% 的场景都用它。只要你的模板需要操作「某种类型」,就用
typename T。 - 非类型参数:当你需要编译期常量时,比如数组大小、位宽、阈值等。性能敏感的场景尤其好用。
- 模板模板参数:当你需要抽象出「容器的种类」而不是「容器的元素类型」时。比如写一个通用的缓存池、对象池。
我的建议: 不要为了炫技而用模板模板参数。如果普通类型参数能解决问题,就别搞复杂了。代码是写给人看的,不是写给编译器看的。
好了,三种模板参数就讲到这里。记住它们的区别和适用场景,你在写泛型代码时会更有底气。下次遇到「这个模板参数到底该用哪种」的问题,回头看看这张图就行。
公众号:蓝海资料掘金营,微信deep3321