18、模板元编程进阶:编译期列表(typelist)、编译期算法、编译期排序

好,咱们今天聊点硬核的。模板元编程(TMP)走到这一步,才算真正进入深水区。

前面我们讲了模板基础、特化、SFINAE,那些说白了是「工具」。今天要讲的,是用这些工具搭建完整的数据结构和算法——在编译期。

你想想看,运行时我们能写链表、排序、查找。编译期能不能?当然能。而且一旦你掌握了编译期列表(typelist)和编译期算法,很多运行时才能暴露的问题,在编译期就能直接扼杀掉。我个人习惯把这种能力叫做「把错误消灭在 -O0 阶段」。

18.1 编译期列表(Typelist)—— 编译期的 std::vector

Typelist 是什么?说白了就是一个类型组成的列表。运行时我们用 vector<int> 存整数,编译期我们用 Typelist<int, double, char> 存类型。

它的定义极其简单:

template<typename... Ts>
struct Typelist {};

嗯,就这?对,就这。但别小看它。空壳子里面能玩出花来。

我在项目中遇到过这样一个场景:需要根据不同的平台(x86、ARM、RISC-V)选择不同的算法实现。如果运行时做 if-else,分支预测失败会掉性能。用 Typelist + 编译期查找,直接生成最优代码路径,零开销。

核心思想:Typelist 本身不存储任何数据,它只承载「类型信息」。所有操作都在类型层面完成。

18.2 编译期算法——遍历、查找、转换

有了列表,自然要有算法。我们来看几个最常用的。

18.2.1 获取列表长度

template<typename TL>
struct Length;

template<typename... Ts>
struct Length<Typelist<Ts...>> {
    static constexpr size_t value = sizeof...(Ts);
};

这里用到了 sizeof... 运算符,C++11 就有了。没什么难度,但它是所有算法的基础。

18.2.2 按索引获取类型

template<size_t N, typename TL>
struct TypeAt;

template<size_t N, typename T, typename... Ts>
struct TypeAt<N, Typelist<T, Ts...>> {
    using type = typename TypeAt<N-1, Typelist<Ts...>>::type;
};

template<typename T, typename... Ts>
struct TypeAt<0, Typelist<T, Ts...>> {
    using type = T;
};

递归展开,每次剥掉第一个类型,直到 N 减到 0。这就是编译期的「数组下标访问」。

避坑指南:我曾经在 TypeAt 里忘记处理 N 越界的情况,结果编译器报了一堆天书般的错误。后来我加了个 static_assert,世界清净了。

18.2.3 编译期查找(Find)

template<typename T, typename TL>
struct IndexOf;

template<typename T, typename... Ts>
struct IndexOf<T, Typelist<T, Ts...>> {
    static constexpr size_t value = 0;
};

template<typename T, typename U, typename... Ts>
struct IndexOf<T, Typelist<U, Ts...>> {
    static constexpr size_t value = 1 + IndexOf<T, Typelist<Ts...>>::value;
};

这个算法在运行时就是线性查找。但在编译期,它没有循环,只有递归模板实例化。编译器会帮你展开成最优形式。

18.3 编译期排序——冒泡排序的模板元实现

好,重头戏来了。编译期排序。

你可能会问:排序不是运行时的事吗?为什么要在编译期做?

原因很简单:类型顺序影响代码生成。比如你要根据类型的对齐要求,从大到小排列,以减少内存填充。这种信息编译期完全可知,为什么不提前排好?

我们以冒泡排序为例,因为它最容易用模板递归实现。

// 辅助:比较两个类型的大小(这里按 sizeof 排序)
template<typename A, typename B>
struct TypeSizeLess {
    static constexpr bool value = (sizeof(A) < sizeof(B));
};

// 一次冒泡:将最大元素冒到末尾
template<typename TL, template<typename, typename> class Compare>
struct BubbleOnce;

template<template<typename, typename> class Compare>
struct BubbleOnce<Typelist<>, Compare> {
    using type = Typelist<>;
};

template<typename T, template<typename, typename> class Compare>
struct BubbleOnce<Typelist<T>, Compare> {
    using type = Typelist<T>;
};

template<typename A, typename B, typename... Rest,
         template<typename, typename> class Compare>
struct BubbleOnce<Typelist<A, B, Rest...>, Compare> {
    using Next = typename BubbleOnce<Typelist<B, Rest...>, Compare>::type;
    using First = typename TypeAt<0, Next>::type;
    using Tail = typename TypeAtHelper<1, Next>::type; // 简化示意
    
    // 如果 A < First,则 A 在前,否则交换
    using type = typename std::conditional<
        Compare<A, First>::value,
        Typelist<A, First, Tail...>,
        Typelist<First, A, Tail...>
    >::type;
};

// 完整排序:重复 BubbleOnce N 次
template<typename TL, template<typename, typename> class Compare>
struct Sort : Sort<typename BubbleOnce<TL, Compare>::type, Compare> {};

template<template<typename, typename> class Compare>
struct Sort<Typelist<>, Compare> {
    using type = Typelist<>;
};

template<typename T, template<typename, typename> class Compare>
struct Sort<Typelist<T>, Compare> {
    using type = Typelist<T>;
};
注意:上面的代码为了可读性做了简化。实际项目中,TypeAtHelper 需要正确处理索引边界。另外,冒泡排序在编译期的时间复杂度是 O(n²) 的模板实例化,n 超过 20 时编译时间会明显增加。我建议用归并排序或快速排序的模板元版本,但实现复杂度会翻倍。

18.4 实战:用编译期排序优化内存布局

我记得有一次做嵌入式开发,需要把一组结构体按对齐要求重新排列。运行时做这件事有额外开销,而且嵌入式设备上 ROM 空间宝贵。

于是我用编译期排序,把类型按 sizeof 从大到小排列,然后生成一个联合体(union)来复用内存。代码大概长这样:

using MyTypes = Typelist<char, double, int, short, long long>;
using SortedTypes = Sort<MyTypes, TypeSizeGreater>::type;
// SortedTypes 现在是 Typelist<long long, double, int, short, char>

// 然后生成 union
template<typename TL>
struct UnionFromTypelist;

template<typename... Ts>
struct UnionFromTypelist<Typelist<Ts...>> {
    union {
        Ts... members;
    };
};

这样生成的 union,内存浪费最小,而且所有工作都在编译期完成。运行时零开销。

18.5 知识体系总览

下面这张图总结了本章的核心脉络:

编译期模板元编程核心体系 Typelist(编译期列表) Length(长度) TypeAt(索引访问) IndexOf(查找) Transform(转换) Filter(过滤) Sort(排序) 应用:内存布局优化 / 策略选择 / 代码生成 所有操作均在编译期完成,运行时零开销

18.6 小结与个人体会

编译期列表和算法,是模板元编程从「玩具」走向「工程」的分水岭。

我个人觉得,掌握 Typelist 的关键不在于记住那些递归模板的写法,而在于建立「类型即数据」的思维模式。一旦你习惯了把类型当作值来操作,很多运行时才能做的事情,你都会下意识地想:能不能提到编译期?

当然,凡事有度。编译期排序的模板实例化深度和编译时间是需要权衡的。我一般只在类型数量不超过 30 个时使用,超过这个数,我会考虑用代码生成器或者运行时一次初始化。

一句话总结:Typelist 是编译期的容器,递归模板是编译期的循环,特化是编译期的分支。三者结合,你可以在编译期实现任何算法。

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