模板参数详解:类型参数与非类型参数、模板模板参数、默认模板参数

大家好,我是你们的老朋友。今天我们来聊聊模板参数——这个看似基础、实则暗藏玄机的话题。

说实话,我见过不少C++开发者,用模板写了好几年,但对模板参数的理解还停留在「类型参数」这个层面。嗯,这其实挺可惜的。因为模板参数的世界远比你想的要丰富。

一、类型参数:最熟悉的陌生人

类型参数,说白了就是模板最核心的用法。你肯定写过这样的代码:

template<typename T>
class Box {
    T value;
public:
    void set(const T& v) { value = v; }
    T get() const { return value; }
};

这里的 T 就是类型参数。你可以传进去 intdoublestd::string,甚至是你自己定义的类。

我个人习惯用 typename 而不是 class,虽然两者在模板参数声明中完全等价。为什么?因为 typename 读起来更直观——「这是一个类型名字」。当然,这只是个人偏好,你用 class 也没问题。

小技巧:当你需要多个类型参数时,命名要有意义。别用 T1T2T3 这种,时间长了你自己都分不清。我一般用 KeyTypeValueTypeElementType 这样的命名。

二、非类型参数:模板里的「常量」

类型参数之外,模板还支持非类型参数。说白了,就是你可以把整数、枚举、指针这些「值」当作模板参数传进去。

template<typename T, size_t Size>
class Array {
    T data[Size];
public:
    size_t size() const { return Size; }
    T& operator[](size_t idx) { return data[idx]; }
};

你看,这里的 Size 就是一个 size_t 类型的非类型参数。用的时候:

Array<int, 10> arr;  // 编译期就知道数组大小是10

为什么会有人喜欢用非类型参数?因为它在编译期就确定了值,没有运行时开销。我在项目中遇到过需要固定大小缓冲区的场景,用非类型参数做模板,既安全又高效。

注意:非类型参数的类型有限制。C++17之前只允许整型、枚举、指针和引用。C++20放宽了限制,允许浮点数和字面量类类型。但别高兴太早——字符串字面量还是不行。

我曾经踩过一个坑:想用字符串作为非类型参数,结果编译器报了一堆看不懂的错误。后来查资料才知道,字符串字面量是链接期才确定的,编译期没法用。

三、模板模板参数:模板的模板

这个就有点意思了。模板模板参数,顾名思义——模板参数本身也是一个模板。

你想想看,有时候你写一个容器适配器,希望用户能指定底层容器类型。比如:

template<typename T, template<typename> class Container>
class Stack {
    Container<T> data;
public:
    void push(const T& v) { data.push_back(v); }
    void pop() { data.pop_back(); }
    T top() const { return data.back(); }
};

这里的 Container 就是一个模板模板参数。它本身是一个模板,接受一个类型参数,返回一个具体的容器类型。

用的时候:

Stack<int, std::vector> s1;   // 底层用vector
Stack<int, std::deque> s2;    // 底层用deque

嗯,这里要注意:std::vector 其实有两个模板参数(元素类型和分配器),但第二个有默认值。所以 template<typename> class Container 能匹配上。

核心要点:模板模板参数让你的代码更加灵活。它允许用户在不改变接口的情况下,替换底层实现。说白了,就是策略模式在编译期的体现。

四、默认模板参数:省心省力的好帮手

默认模板参数,跟函数默认参数一个道理——你不传,我就用默认值。

template<typename T = int, size_t Size = 100>
class Buffer {
    T data[Size];
public:
    // ...
};

用的时候:

Buffer<> default_buffer;           // T=int, Size=100
Buffer<double> double_buffer;      // T=double, Size=100
Buffer<char, 256> char_buffer;     // T=char, Size=256

我个人建议,默认模板参数用在「大多数情况下都够用」的场景。比如容器的分配器参数,99%的人用默认的 std::allocator 就够了。

但有一个坑:默认模板参数必须从右往左依次提供。你不能跳过前面的参数去指定后面的。比如:

template<typename T, typename Alloc = std::allocator<T>>
class MyVector { /* ... */ };

MyVector<int> v;  // 正确,Alloc用默认值
// MyVector<int, , std::greater<int>> v;  // 错误!不能跳过

五、知识体系总览

说了这么多,我们来画个图,把今天的内容串起来:

模板参数知识体系 模板参数 类型参数 非类型参数 模板模板参数 默认模板参数 typename T class T 整型 枚举/指针 template<typename> T = int Size = 100 四种参数各司其职,组合使用让模板更灵活、更强大

六、组合使用:让模板更灵活

这些参数类型不是孤立的,它们可以组合使用。比如:

template<
    typename T,
    size_t Size = 64,
    template<typename> class Container = std::vector
>
class RingBuffer {
    Container<T> data;
    // 使用Size作为缓冲区大小
};

你看,类型参数、非类型参数、模板模板参数、默认模板参数,全用上了。这就是模板参数的全部威力。

避坑指南:我曾经写过一个模板,默认模板参数依赖前面的类型参数。结果用户传了一个自定义类型,默认分配器不匹配,编译报错信息长达几百行。后来我加了一个static_assert,提前检查类型兼容性,问题就解决了。

好了,今天的内容就到这里。模板参数是泛型编程的基石,理解透了,后面的路就好走了。


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