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 的关系:
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++ 的哲学。