写模板代码时常有这样的需求:手里有一组类型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>structtypelist{};// 空壳:只「携带」类型,不产生任何对象usingMyTypes=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>structtypelist{};// length:类型个数template<typenameList>structlength;template<typename...Ts>structlength<typelist<Ts...>>{staticconstexprstd::size_t value=sizeof...(Ts);};// at:第 N 个类型(递归:N 每层减 1,同时砍掉头部)template<std::size_t N,typenameList>structat;template<typenameHead,typename...Tail>structat<0,typelist<Head,Tail...>>{usingtype=Head;};// 递归基:N==0template<std::size_t N,typenameHead,typename...Tail>structat<N,typelist<Head,Tail...>>{usingtype=typenameat<N-1,typelist<Tail...>>::type;};// append:尾部追加一个类型template<typenameList,typenameT>structappend;template<typename...Ts,typenameT>structappend<typelist<Ts...>,T>{usingtype=typelist<Ts...,T>;};intmain(){usingL=typelist<int,double,char,long>;static_assert(length<L>::value==4,"长度应为 4");static_assert(std::is_same_v<typenameat<0,L>::type,int>,"第 0 个是 int");static_assert(std::is_same_v<typenameat<2,L>::type,char>,"第 2 个是 char");usingL2=typenameappend<L,float>::type;static_assert(length<L2>::value==5,"追加后长度 5");static_assert(std::is_same_v<typenameat<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>structtypelist{};// prepend:头部插入(filter 回溯时用它把「保留」的 Head 拼回去)template<typenameList,typenameT>structprepend;template<typename...Ts,typenameT>structprepend<typelist<Ts...>,T>{usingtype=typelist<T,Ts...>;};// map:对每个类型应用元函数 F(取 F<T>::type)template<typenameList,template<typename>classF>structmap;template<typename...Ts,template<typename>classF>structmap<typelist<Ts...>,F>{usingtype=typelist<typenameF<Ts>::type...>;};// filter:递归,保留满足 Pred 的类型template<typenameList,template<typename>classPred>structfilter;template<template<typename>classPred>structfilter<typelist<>,Pred>{usingtype=typelist<>;};// 递归基:空列表template<typenameHead,typename...Tail,template<typename>classPred>structfilter<typelist<Head,Tail...>,Pred>{usingrest=typenamefilter<typelist<Tail...>,Pred>::type;// 先处理尾巴usingtype=std::conditional_t<Pred<Head>::value,typenameprepend<rest,Head>::type,rest>;};intmain(){usingL=typelist<int,char,double,long,float>;usingP=typenamemap<L,std::add_pointer>::type;static_assert(std::is_same_v<P,typelist<int*,char*,double*,long*,float*>>,"map 后应全是指针");usingI=typenamefilter<L,std::is_integral>::type;static_assert(std::is_same_v<I,typelist<int,char,long>>,"filter 后应只剩整型");usingIP=typenamemap<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>structtypelist{};// typelist -> std::tupletemplate<typenameList>structto_tuple;template<typename...Ts>structto_tuple<typelist<Ts...>>{usingtype=std::tuple<Ts...>;};// 编译期聚合:所有类型的 sizeof 之和(C++17 折叠表达式)template<typenameList>structtotal_size;template<typename...Ts>structtotal_size<typelist<Ts...>>{staticconstexprstd::size_t value=(sizeof(Ts)+...+std::size_t{0});};intmain(){usingL=typelist<int,double,char,long>;usingT=typenameto_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 之和: 21total_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的源码,这套手艺仍然是绕不开的底子。