写模板代码时常有这样的需求:手里有一组类型 int, double, char, long,想「给每个类型生成一个成员变量」「把每个类型变成它的指针」「只留下整型」…… 这些运算发生在编译期,操作对象是「类型」而不是「值」。C++ 里表示「一组类型」的惯用法叫 typelist(类型列表)——它就是一个 template<class...> struct typelist {} 的空壳,真正的功夫全在编译期算法上。这些算法在 boost::mp11、std::tuple 的实现里到处都是,读懂它才能看懂那些库。
1. 引子:为什么需要「类型列表」
std::vector<int> 存的是「一堆 int 值」,而 typelist 存的是「一堆类型」。它本身不占内存、不产生任何对象,纯粹是给编译器看的。
最典型的应用场景:tuple 的迭代、variant 的类型集合、给每个类型生成一个字段的序列化框架。比如你想写一个结构体,它的成员是 int、double、string 各一份,模板代码里就需要先「拿到这组类型」,再逐个展开。typelist 就是那个「拿在手里的类型集合」。
template <typename... Ts>
struct typelist {}; // 空壳:只「携带」类型,不产生任何对象
using MyTypes = typelist<int, double, char, long>;
一个 typelist 就是一个类型,里面的 Ts... 是类型参数包(parameter pack)。要对它做运算,就要用**偏特化(partial specialization)**去「解包」它。
官方文档:类型参数包与包展开(parameter pack) —— typelist 和所有变参模板的根基。
2. 基础算法:length / at / append
这三个是最基本的「列表操作」,套路统一:声明主模板 → 偏特化匹配 typelist<Ts...> → 用 sizeof...(Ts) 或递归解出结果。
#include <cstdio>
#include <cstddef>
#include <type_traits>
template <typename... Ts> struct typelist {};
// length:类型个数
template <typename List> struct length;
template <typename... Ts>
struct length<typelist<Ts...>> { static constexpr std::size_t value = sizeof...(Ts); };
// at:第 N 个类型(递归:N 每层减 1,同时砍掉头部)
template <std::size_t N, typename List> struct at;
template <typename Head, typename... Tail>
struct at<0, typelist<Head, Tail...>> { using type = Head; }; // 递归基:N==0
template <std::size_t N, typename Head, typename... Tail>
struct at<N, typelist<Head, Tail...>> { using type = typename at<N - 1, typelist<Tail...>>::type; };
// append:尾部追加一个类型
template <typename List, typename T> struct append;
template <typename... Ts, typename T>
struct append<typelist<Ts...>, T> { using type = typelist<Ts..., T>; };
int main() {
using L = typelist<int, double, char, long>;
static_assert(length<L>::value == 4, "长度应为 4");
static_assert(std::is_same_v<typename at<0, L>::type, int>, "第 0 个是 int");
static_assert(std::is_same_v<typename at<2, L>::type, char>, "第 2 个是 char");
using L2 = typename append<L, float>::type;
static_assert(length<L2>::value == 5, "追加后长度 5");
static_assert(std::is_same_v<typename at<4, L2>::type, float>, "第 4 个是 float");
std::puts("所有 static_assert 通过:length / at / append 正确");
}
所有 static_assert 通过:length / at / append 正确
要点:at<N, typelist<Head, Tail...>> 的递归是「每层丢掉 Head,N 减 1」,直到 N==0 时命中递归基,返回当前的 Head。这就是函数式编程里「头尾分解」在模板里的翻版。
官方文档:偏特化(partial specialization) —— 解包 typelist 必须靠它,
at的递归基at<0, ...>就是更特化的匹配。
3. 核心:map 与 filter
有了头尾分解,就能写「高阶」编译期算法:
- map:对每个类型套一个元函数
F,得到新列表。map<typelist<int,char>, add_pointer>→typelist<int*,char*>。 - filter:只保留满足谓词
Pred的类型。filter<typelist<int,char,double>, is_integral>→typelist<int,char>。
#include <cstdio>
#include <cstddef>
#include <type_traits>
template <typename... Ts> struct typelist {};
// prepend:头部插入(filter 回溯时用它把「保留」的 Head 拼回去)
template <typename List, typename T> struct prepend;
template <typename... Ts, typename T>
struct prepend<typelist<Ts...>, T> { using type = typelist<T, Ts...>; };
// map:对每个类型应用元函数 F(取 F<T>::type)
template <typename List, template <typename> class F> struct map;
template <typename... Ts, template <typename> class F>
struct map<typelist<Ts...>, F> { using type = typelist<typename F<Ts>::type...>; };
// filter:递归,保留满足 Pred 的类型
template <typename List, template <typename> class Pred> struct filter;
template <template <typename> class Pred>
struct filter<typelist<>, Pred> { using type = typelist<>; }; // 递归基:空列表
template <typename Head, typename... Tail, template <typename> class Pred>
struct filter<typelist<Head, Tail...>, Pred> {
using rest = typename filter<typelist<Tail...>, Pred>::type; // 先处理尾巴
using type = std::conditional_t<Pred<Head>::value,
typename prepend<rest, Head>::type,
rest>;
};
int main() {
using L = typelist<int, char, double, long, float>;
using P = typename map<L, std::add_pointer>::type;
static_assert(std::is_same_v<P, typelist<int*, char*, double*, long*, float*>>,
"map 后应全是指针");
using I = typename filter<L, std::is_integral>::type;
static_assert(std::is_same_v<I, typelist<int, char, long>>,
"filter 后应只剩整型");
using IP = typename map<I, std::add_pointer>::type;
static_assert(std::is_same_v<IP, typelist<int*, char*, long*>>,
"map(filter) 组合:整型指针列表");
std::puts("所有 static_assert 通过:map / filter 正确");
}
所有 static_assert 通过:map / filter 正确
map 一步到位:直接 typelist<typename F<Ts>::type...> 用包展开套用 F。filter 则必须递归:先递归处理尾巴 Tail... 得到 rest,再根据 Pred<Head>::value 决定要不要用 prepend 把 Head 插回头部。递归的展开过程如下:
filter< typelist<int, char, double>, std::is_integral > 的递归展开
filter<int, char, double>
├─ Head = int is_integral<int> = true → 插回头部
└─ filter<char, double>
├─ Head = char is_integral<char> = true → 插回头部
└─ filter<double>
├─ Head = double is_integral<double> = false → 丢弃
└─ filter<> (递归基,返回 typelist<>)
回溯:typelist<> → typelist<char> → typelist<int, char> (最终结果)
注意回溯的顺序:因为每一步都用 prepend 往头部插,所以结果保持了原列表的相对顺序。这是 filter 实现里最容易写反的地方(用 append 会得到反序列表)。
官方文档:std::conditional(编译期 if) —— filter 里「保留还是丢弃」靠它做编译期分支。
4. typelist 与 std::tuple 的关系
两者长得像,但分工完全不同:
| 维度 | typelist | std::tuple |
|---|---|---|
| 存的是什么 | 纯类型,没有值 | 类型对应的实例(值) |
| 运行时机 | 纯编译期,零运行时开销 | 运行期对象 |
| 能否 sizeof | 否(不是对象) | 是,占内存 |
| 典型用途 | 类型运算:map / filter / at | 存异构数据、多返回值、参数打包 |
| 转换 | to_tuple<typelist<Ts...>>::type = std::tuple<Ts...> | std::tuple_element_t 取回类型 |
简单说:typelist 是「类型的清单」,tuple 是「把清单落地成值」。很多库(比如 boost::mp11)就是先用 typelist 算好类型,再转成 tuple 或结构体去用。
5. 落地:把 typelist 变成能用的东西
typelist 本身不能跑,最终要「落地」。下面是两个典型落地方式:转成 std::tuple,以及在编译期对类型做聚合计算(这里用折叠表达式求所有类型 sizeof 之和)。
#include <cstdio>
#include <cstddef>
#include <tuple>
#include <type_traits>
template <typename... Ts> struct typelist {};
// typelist -> std::tuple
template <typename List> struct to_tuple;
template <typename... Ts>
struct to_tuple<typelist<Ts...>> { using type = std::tuple<Ts...>; };
// 编译期聚合:所有类型的 sizeof 之和(C++17 折叠表达式)
template <typename List> struct total_size;
template <typename... Ts>
struct total_size<typelist<Ts...>> {
static constexpr std::size_t value = (sizeof(Ts) + ... + std::size_t{0});
};
int main() {
using L = typelist<int, double, char, long>;
using T = typename to_tuple<L>::type;
static_assert(std::is_same_v<T, std::tuple<int, double, char, long>>,
"typelist 应能落地成 tuple");
T t{42, 3.14, 'x', 99L};
std::printf("tuple 元素个数: %zu\n", std::tuple_size_v<T>);
std::printf("第 0 个元素: %d\n", std::get<0>(t));
std::printf("第 1 个元素: %.2f\n", std::get<1>(t));
std::printf("所有类型 sizeof 之和: %zu\n", total_size<L>::value);
}
tuple 元素个数: 4
第 0 个元素: 42
第 1 个元素: 3.14
所有类型 sizeof 之和: 21
total_size 里的 (sizeof(Ts) + ... + std::size_t{0}) 是 C++17 的折叠表达式(fold expression),把「对每个类型算 sizeof 再累加」这件事一行写完——这也是 typelist 类算法在现代 C++ 里被逐步替代的信号。
官方文档:std::tuple — cppreference —— typelist 最常见的落地目标。
6. 现状:Concepts 与折叠表达式取代了什么
手写 typelist + 递归 map/filter 是 C++11 时代的主流,但现在有了更顺手的工具,很多场景不必再手写:
| 需求 | 老办法(typelist 递归) | 新办法 |
|---|---|---|
| 对每个类型做一件事 | 递归 + 偏特化 | 折叠表达式 (f(Ts), ...) |
| 编译期 if | std::conditional / tag dispatch | if constexpr(C++17) |
| 约束模板参数 | SFINAE / enable_if | Concepts(C++20) |
| 类型集合运算(map/filter/at) | 手写 typelist | boost::mp11 |
不过有两个理由仍然要读懂 typelist:第一,boost::mp11、std::variant、std::tuple 这些库内部就是这套东西,读源码绕不开;第二,std::tuple_element_t<N, Tuple>、std::variant 的类型运算,本质就是 typelist 的 at。理解了底层的「头尾分解 + 递归 + 偏特化」,再看到 mp_list 那种高级写法就不会发怵。
7. 延伸阅读
- 类型参数包(parameter pack)— cppreference:typelist 与包展开的地基
- 偏特化(partial specialization)— cppreference:
at/filter的递归基就是靠它「更特化优先」匹配 - std::conditional — cppreference:编译期分支的核心工具
- std::tuple — cppreference:typelist 最常见的落地目标,配合
std::tuple_element就是内置的at
本知识库内的相关篇目:
- 《tag dispatch 与重载偏序实战:按迭代器类别派发算法》 —— tag dispatch(标签派发)用「空类型」当标签
- 《表达式模板:Eigen 式延迟求值,把 a+b+c 压成一次遍历》 —— 表达式模板(Expression Templates)让 a+b+c 不再生成中间临时对象
- 《编译期反射模拟:C++ 没有反射怎么办》 —— C++ 至今没有运行时反射,想「遍历结构体成员」「自动序列化」只能靠模拟
8. 一句话总结
typelist 用一个空壳 template<class...> struct typelist 携带一组类型,length/at/map/filter 全靠「头尾分解 + 递归 + 偏特化」实现,结果用 static_assert 在编译期验证;现代 C++ 里它正被 Concepts、折叠表达式、if constexpr 部分取代,但读 boost::mp11、std::tuple 的源码,这套手艺仍然是绕不开的底子。
389



被折叠的 条评论
为什么被折叠?



