第三章:模板参数进阶——非类型模板参数、模板模板参数、可变参数模板的声明与展开
模板参数,说白了就是模板的「输入」。大多数人都用过类型模板参数,比如 typename T。但C++模板的能力远不止于此。我个人习惯把模板参数分成三类:类型参数、非类型参数、模板参数。今天我们就来聊聊后两种,再加上一个让模板变得极其灵活的可变参数模板。
3.1 非类型模板参数:不只是类型
模板参数不一定非得是类型。它可以是一个整数、一个枚举、一个指针,甚至是一个 std::nullptr_t。这就是非类型模板参数。
举个例子,你想写一个固定大小的数组包装器:
template<typename T, std::size_t Size>
class FixedArray {
T data[Size];
public:
constexpr std::size_t size() const { return Size; }
T& operator[](std::size_t i) { return data[i]; }
};
// 使用
FixedArray<int, 10> arr; // 编译期就知道大小
我在项目中遇到过一个问题:用 std::array 时,大小必须是编译期常量。非类型模板参数正好解决了这个痛点。你想想看,如果大小是运行时传入的,那编译器就没法做优化了。
核心要点:非类型模板参数必须是编译期常量表达式。支持的类型包括:整型、枚举、指针、引用、std::nullptr_t,以及C++20开始支持的浮点型和字面量类类型。
避坑指南:我曾经在项目里用 double 作为非类型模板参数,结果发现C++20之前是不支持的。如果你还在用C++17,老老实实用整型吧。
3.2 模板模板参数:模板的模板
模板模板参数,顾名思义,就是模板参数本身也是一个模板。听起来有点绕,但用起来很直观。
假设你想写一个容器适配器,它接受一个容器类型作为参数:
template<typename T, template<typename> class Container>
class Adapter {
Container<T> container;
public:
void push(const T& value) { container.push_back(value); }
// ...
};
// 使用
Adapter<int, std::vector> vecAdapter; // 注意:std::vector有两个模板参数,这里需要适配
嗯,这里要注意。标准库容器通常有多个模板参数(比如分配器)。直接传 std::vector 会编译失败,因为 std::vector 实际上需要两个参数。解决办法是使用别名模板:
template<typename T>
using MyVector = std::vector<T, std::allocator<T>>;
Adapter<int, MyVector> adapter;
我个人习惯在写泛型库时,尽量让模板模板参数灵活一些。比如允许用户指定默认分配器,而不是硬编码。
3.3 可变参数模板:数量不定的参数
可变参数模板是C++11引入的,它允许模板接受任意数量的参数。说白了,就是参数包(parameter pack)。
template<typename... Args>
void print(Args... args) {
// args 是一个参数包
}
这里 Args 是类型参数包,args 是函数参数包。你可以传任意数量的参数:
print(1, 2.0, "hello", 'c'); // 四个参数,类型各不相同
3.4 参数包的展开:核心难点
参数包本身不能直接使用,必须展开。展开的方式有很多种,我挑几个最常用的讲。
3.4.1 递归展开
这是最经典的方式,通过递归函数模板来逐个处理参数:
// 递归终止条件
void print() {}
// 递归展开
template<typename T, typename... Args>
void print(const T& first, const Args&... rest) {
std::cout << first << " ";
print(rest...); // 展开 rest 包
}
// 调用
print(1, 2.0, "hello"); // 输出:1 2 hello
我在项目中遇到过一个问题:递归展开在参数很多时会导致模板实例化爆炸,编译时间会变长。后来我改用折叠表达式(C++17),效率高多了。
3.4.2 折叠表达式(C++17)
折叠表达式让参数包展开变得极其简洁。它分为左折叠和右折叠:
// 一元右折叠
template<typename... Args>
auto sum(Args... args) {
return (args + ...); // 展开为 ((a + b) + c) + ...
}
// 一元左折叠
template<typename... Args>
auto sumLeft(Args... args) {
return (... + args); // 展开为 a + (b + (c + ...))
}
// 二元折叠(带初始值)
template<typename... Args>
auto sumWithInit(Args... args) {
return (0 + ... + args); // 初始值0
}
小技巧:折叠表达式支持几乎所有二元运算符。比如用 && 做逻辑与,用 , 做逗号表达式。我个人最常用的是逗号表达式来执行多个操作:
template<typename... Args>
void printAll(Args... args) {
((std::cout << args << " "), ...); // 逗号折叠
}
3.4.3 模式展开
有时候你需要对每个参数应用一个模式。比如把每个参数都包装成 std::make_tuple:
template<typename... Args>
auto makeTupleOfTuples(Args... args) {
return std::make_tuple(std::make_tuple(args)...);
// 展开为 std::make_tuple(std::make_tuple(a), std::make_tuple(b), ...)
}
你想想看,这种模式展开在元编程中非常有用。比如你想对每个类型应用一个元函数:
template<typename... Types>
struct TypeList {
// 对每个类型应用 std::add_pointer
using PointerList = std::tuple<typename std::add_pointer<Types>::type...>;
};
3.5 综合示例:一个简单的元组实现
把上面的知识串起来,我们实现一个简化版的 std::tuple:
// 基础模板
template<typename... Types>
class Tuple;
// 递归终止:空元组
template<>
class Tuple<> {};
// 递归定义
template<typename Head, typename... Tail>
class Tuple<Head, Tail...> : private Tuple<Tail...> {
Head value;
public:
Tuple() = default;
Tuple(Head h, Tail... t) : Tuple<Tail...>(t...), value(h) {}
Head& getHead() { return value; }
const Head& getHead() const { return value; }
Tuple<Tail...>& getTail() { return *this; }
const Tuple<Tail...>& getTail() const { return *this; }
};
// 获取第N个元素的辅助函数
template<std::size_t Index, typename... Types>
auto& get(Tuple<Types...>& t) {
if constexpr (Index == 0) {
return t.getHead();
} else {
return get<Index - 1>(t.getTail());
}
}
这个例子用到了非类型模板参数(std::size_t Index)、可变参数模板(typename... Types)、以及递归展开。我在实际项目中就用类似的方式实现过一个轻量级的类型安全容器。
3.6 知识体系总览
下面这张图总结了本章的核心知识点和它们之间的关系:
3.7 实战建议
学完这些知识,你可能会问:什么时候用?我给出几个实际场景:
- 非类型模板参数:编译期常量,比如数组大小、位宽、循环次数。我在写嵌入式代码时经常用它来配置硬件寄存器地址。
- 模板模板参数:当你需要抽象出容器或策略时。比如写一个通用的缓存系统,允许用户指定底层存储结构。
- 可变参数模板:实现类型安全的变参函数、元组、访问者模式。说白了,只要参数数量不确定,就用它。
性能提醒:可变参数模板的递归展开会导致大量模板实例化。如果参数数量很大(比如超过100个),编译时间和二进制体积会显著增加。我曾经在一个日志库中用了递归展开,结果编译时间从10秒飙到了3分钟。后来改用折叠表达式,问题解决了。
好了,这一章的内容就到这里。记住,模板参数进阶是元编程的基石。掌握了非类型参数、模板模板参数和可变参数模板,你就能写出更通用、更灵活的代码。