模板参数详解:类型参数与非类型参数、模板模板参数、默认模板参数
大家好,我是你们的老朋友。今天我们来聊聊模板参数——这个看似基础、实则暗藏玄机的话题。
说实话,我见过不少C++开发者,用模板写了好几年,但对模板参数的理解还停留在「类型参数」这个层面。嗯,这其实挺可惜的。因为模板参数的世界远比你想的要丰富。
一、类型参数:最熟悉的陌生人
类型参数,说白了就是模板最核心的用法。你肯定写过这样的代码:
template<typename T>
class Box {
T value;
public:
void set(const T& v) { value = v; }
T get() const { return value; }
};
这里的 T 就是类型参数。你可以传进去 int、double、std::string,甚至是你自己定义的类。
我个人习惯用 typename 而不是 class,虽然两者在模板参数声明中完全等价。为什么?因为 typename 读起来更直观——「这是一个类型名字」。当然,这只是个人偏好,你用 class 也没问题。
T1、T2、T3 这种,时间长了你自己都分不清。我一般用 KeyType、ValueType、ElementType 这样的命名。
二、非类型参数:模板里的「常量」
类型参数之外,模板还支持非类型参数。说白了,就是你可以把整数、枚举、指针这些「值」当作模板参数传进去。
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
为什么会有人喜欢用非类型参数?因为它在编译期就确定了值,没有运行时开销。我在项目中遇到过需要固定大小缓冲区的场景,用非类型参数做模板,既安全又高效。
我曾经踩过一个坑:想用字符串作为非类型参数,结果编译器报了一堆看不懂的错误。后来查资料才知道,字符串字面量是链接期才确定的,编译期没法用。
三、模板模板参数:模板的模板
这个就有点意思了。模板模板参数,顾名思义——模板参数本身也是一个模板。
你想想看,有时候你写一个容器适配器,希望用户能指定底层容器类型。比如:
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; // 错误!不能跳过
五、知识体系总览
说了这么多,我们来画个图,把今天的内容串起来:
六、组合使用:让模板更灵活
这些参数类型不是孤立的,它们可以组合使用。比如:
template<
typename T,
size_t Size = 64,
template<typename> class Container = std::vector
>
class RingBuffer {
Container<T> data;
// 使用Size作为缓冲区大小
};
你看,类型参数、非类型参数、模板模板参数、默认模板参数,全用上了。这就是模板参数的全部威力。
好了,今天的内容就到这里。模板参数是泛型编程的基石,理解透了,后面的路就好走了。