14. 模板元编程基础:编译期控制结构(if/else、循环)、编译期容器(Typelist)

模板元编程,说白了就是让编译器替你干活。你写一套逻辑,编译器在编译阶段就把结果算出来。我第一次接触这个概念时,觉得这玩意儿有点反直觉——代码还没跑,结果就定了?后来在项目中做高性能计算库时,才真正体会到它的威力。

这一章,我们聚焦三个核心:编译期的条件判断、循环,以及一个非常实用的工具——Typelist。掌握了这些,你就能在编译期完成很多原本只能在运行时做的事。

14.1 编译期 if/else:std::conditional 与 SFINAE

运行时我们有 if-else,编译期呢?C++ 提供了 std::conditional,它就像编译期的三目运算符。

#include <type_traits>

// 如果条件为真,类型为 int;否则为 double
using SelectedType = std::conditional_t<true, int, double>;
// SelectedType 就是 int

嗯,这里要注意:std::conditional 只做类型选择,不做逻辑分支。如果你想根据条件启用或禁用某个函数,那就得靠 SFINAE(Substitution Failure Is Not An Error)。

核心思想:模板实例化时,如果某个替换导致非法代码,编译器不会报错,只是把这个版本从重载集中移除。

我在项目中遇到过这样一个场景:需要根据类型是否有 size() 方法来选择不同的处理逻辑。用 SFINAE 就能优雅解决:

template<typename T, typename = void>
struct has_size : std::false_type {};

template<typename T>
struct has_size<T, std::void_t<decltype(std::declval<T>().size())>> : std::true_type {};

// 使用
static_assert(has_size<std::vector<int>>::value, "vector should have size()");
static_assert(!has_size<int>::value, "int should not have size()");

为什么用 std::void_t?它能把任何类型变成 void,方便我们做检测。说白了,这就是编译期的「试错」机制。

14.2 编译期循环:递归模板与折叠表达式

运行时循环用 for、while,编译期循环靠递归模板实例化。你想想看,编译器在展开模板时,其实就是在做循环。

一个经典的例子:编译期计算阶乘。

template<unsigned N>
struct Factorial {
    static constexpr unsigned value = N * Factorial<N - 1>::value;
};

template<>
struct Factorial<0> {
    static constexpr unsigned value = 1;
};

// 使用
static_assert(Factorial<5>::value == 120, "5! should be 120");

这里有个关键点:递归终止条件。没有特化版本 Factorial<0>,编译器就会无限递归下去,直到达到模板实例化深度上限。我曾经在项目中因为忘了写终止条件,导致编译时间暴涨到几分钟——嗯,从那以后我再也不敢漏掉特化了。

C++17 引入了折叠表达式,让编译期循环更简洁:

template<typename... Args>
constexpr auto sum(Args... args) {
    return (args + ...);  // 一元右折叠
}

static_assert(sum(1, 2, 3, 4) == 10, "sum should be 10");

折叠表达式说白了就是把参数包用操作符「折叠」起来。上面的 (args + ...) 展开成 1 + (2 + (3 + 4))。我个人习惯用右折叠,但左折叠 (... + args) 也完全没问题,只是结合顺序不同。

小技巧:如果你需要遍历 Typelist 中的每个类型,折叠表达式配合逗号操作符非常实用:((do_something<T>()), ...);

14.3 编译期容器:Typelist

运行时我们有 vector、list,编译期我们有 Typelist。它就是一个类型列表,在编译期操作。

一个简单的 Typelist 定义:

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

// 一些基本操作
template<typename TL>
struct Length;

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

// 获取第一个元素
template<typename TL>
struct Front;

template<typename T, typename... Types>
struct Front<Typelist<T, Types...>> {
    using type = T;
};

// 使用
using MyList = Typelist<int, double, char>;
static_assert(Length<MyList>::value == 3, "length should be 3");
static_assert(std::is_same_v<Front<MyList>::type, int>, "front should be int");

你想想看,Typelist 的操作本质上就是模式匹配。通过模板特化,我们可以提取、修改、组合类型列表。我在写一个序列化库时,就用 Typelist 来管理不同类型的序列化策略,编译期就能确定每个字段的处理方式,运行时零开销。

更高级的操作,比如在 Typelist 中查找某个类型:

template<typename TL, typename T>
struct Contains;

template<typename T, typename... Types>
struct Contains<Typelist<T, Types...>, T> : std::true_type {};

template<typename U, typename T, typename... Types>
struct Contains<Typelist<U, Types...>, T> : Contains<Typelist<Types...>, T> {};

template<typename T>
struct Contains<Typelist<>, T> : std::false_type {};

static_assert(Contains<MyList, double>::value, "should contain double");
static_assert(!Contains<MyList, float>::value, "should not contain float");

注意:Typelist 的递归操作会生成大量模板实例。如果列表太长(比如超过 100 个类型),编译时间和内存消耗会显著增加。我建议在实际项目中控制 Typelist 的大小,或者使用 C++17 的 if constexpr 来减少实例化分支。

14.4 知识体系总览

下面这张图总结了本章的核心内容,帮你理清编译期控制结构和 Typelist 的关系:

编译期元编程核心结构 编译期 if/else std::conditional SFINAE 技术 std::enable_if 编译期循环 递归模板实例化 折叠表达式 (C++17) constexpr 函数 Typelist 容器 类型列表定义 模式匹配操作 编译期算法 三者结合:在编译期完成类型计算、策略选择、 代码生成,实现运行时零开销抽象

14.5 实战:编译期类型过滤器

把上面的知识串起来,我们写一个编译期过滤器——从一个 Typelist 中选出所有满足某个条件的类型。

// 条件:判断类型是否是指针
template<typename T>
struct is_pointer_type : std::false_type {};

template<typename T>
struct is_pointer_type<T*> : std::true_type {};

// 过滤器
template<typename TL, template<typename> class Pred>
struct Filter;

template<template<typename> class Pred>
struct Filter<Typelist<>, Pred> {
    using type = Typelist<>;
};

template<typename T, typename... Types, template<typename> class Pred>
struct Filter<Typelist<T, Types...>, Pred> {
    using type = std::conditional_t<
        Pred<T>::value,
        typename Prepend<T, typename Filter<Typelist<Types...>, Pred>::type>::type,
        typename Filter<Typelist<Types...>, Pred>::type
    >;
};

// 辅助:在 Typelist 前面添加一个类型
template<typename T, typename TL>
struct Prepend;

template<typename T, typename... Types>
struct Prepend<T, Typelist<Types...>> {
    using type = Typelist<T, Types...>;
};

// 使用
using MixedList = Typelist<int, double*, char, float*, void>;
using PointerList = Filter<MixedList, is_pointer_type>::type;
// PointerList 包含 double* 和 float*

这段代码里,std::conditional_t 就是编译期的 if-else,Filter 的递归就是编译期的循环,而 Typelist 就是容器。三者配合,在编译期完成了类型筛选。

个人经验:我在写 ORM 框架时,用这种技术自动生成数据库字段的映射代码。每个字段类型在编译期就被分析、转换,运行时只需要执行预先生成的策略,性能非常好。

模板元编程初看有点绕,但一旦上手,你会发现它让代码更安全、更高效。记住:编译期能做的事,绝不拖到运行时。这就是 C++ 的哲学。

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