模板参数详解:类型参数、非类型参数、模板模板参数

模板参数这个话题,说实话,很多人学了很久 C++ 都只用了最基础的那一层。我见过不少项目,模板参数清一色全是 typename T,然后就没有然后了。其实模板参数远不止这么简单——类型参数、非类型参数、模板模板参数,这三者构成了 C++ 泛型编程的完整骨架。

今天我就带你把这三种参数彻底捋清楚。嗯,咱们一个一个来。

1. 类型参数:最熟悉的陌生人

类型参数,说白了就是用 typenameclass 声明的参数。它代表一个类型,可以是 int、string、自定义类,甚至是另一个模板。

template <typename T>
class Wrapper {
    T value_;
public:
    explicit Wrapper(T v) : value_(v) {}
    T get() const { return value_; }
};

这里 T 就是类型参数。你传什么类型进去,Wrapper 就变成什么类型的容器。

关键点: typenameclass 在声明类型参数时完全等价。我个人习惯用 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 轻量多了——栈上分配,没有堆开销。

小技巧: 非类型参数可以是 intcharboolenum,甚至 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::vectorstd::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> 泛型容器、算法
非类型参数 intsize_t template<typename T, size_t N> 编译期常量、数组大小
模板模板参数 template<typename> class template<typename T, template<typename> class C> 容器适配器、策略模式

5. 知识体系结构图

下面这张图帮你理清三种参数的关系和适用场景:

模板参数 类型参数 非类型参数 模板模板参数 typename / class 声明 嵌套类型需 typename 前缀 泛型容器、算法 int / size_t / char / bool 编译期常量表达式 固定大小数组、编译期计算 template<typename> class 容器适配器、策略模式 别名模板解决多参数问题

6. 实战中的选择建议

这三种参数怎么选?我个人的经验是:

  • 类型参数:90% 的场景都用它。只要你的模板需要操作「某种类型」,就用 typename T
  • 非类型参数:当你需要编译期常量时,比如数组大小、位宽、阈值等。性能敏感的场景尤其好用。
  • 模板模板参数:当你需要抽象出「容器的种类」而不是「容器的元素类型」时。比如写一个通用的缓存池、对象池。

我的建议: 不要为了炫技而用模板模板参数。如果普通类型参数能解决问题,就别搞复杂了。代码是写给人看的,不是写给编译器看的。

好了,三种模板参数就讲到这里。记住它们的区别和适用场景,你在写泛型代码时会更有底气。下次遇到「这个模板参数到底该用哪种」的问题,回头看看这张图就行。


公众号:蓝海资料掘金营,微信deep3321