adapting c++20 ranges algorithms for most metaprogramming
内容提要
在cppnow 2023的演讲中,daisy展示了简化模板元算法的技巧。她以排序为例,通过将type_list中的每个Type映射到它们的index,然后对index数组进行排序,最终重新安放type_list中的元素。她展示了一个巧妙的排序算法,使用variant数组和std::visit访问数组中的元素。通过使用std::type_identity作为仿函数参数,解决了一些类型无法作为参数的问题。最后,她展示了如何根据类型的大小对type_list中的类型进行排序。
延伸解读
间接排序:用索引数组规避类型直接操作
该方法不直接对类型列表排序,而是先为每个类型生成索引,对索引数组调用std::ranges::sort,再按排序后的索引重排类型。这样把类型层面的比较转化为对整数索引的排序,比较逻辑通过访问索引对应的类型信息实现,降低了模板元编程中直接操作类型的复杂度。
variant与type_identity:安全访问类型信息
比较器构造一个std::variant数组,每个variant项对应一个std::type_identity<T>,再用std::visit取出第i和第j项的类型信息进行比较。使用type_identity<T>而非T作为仿函数参数,是因为T可能是void、数组或不完整类型,不能直接作为函数参数,而type_identity<T>可以安全传递。
constexpr lambda:编译时排序的关键
比较器lambda被声明为constexpr,因此可以在编译期调用。整个排序过程在constexpr上下文中完成,最终结果通过static_assert验证。这表明该技巧依赖C++20的constexpr lambda和ranges算法,适合在编译期对类型列表进行排序等元算法操作。
Q&A
如何用C++20 ranges对type_list中的类型进行排序?
基本思路是将type_list中的每个类型映射到它们的索引,然后对索引数组使用std::ranges::sort进行排序,得到sorted_indices,最后根据sorted_indices重新安放type_list中的元素,返回排序后的type_list。
为什么排序type_list时比较器要用std::variant和std::visit?
因为需要在编译期根据索引i和j获取type_list中对应类型的信息。通过构造一个包含所有类型的std::variant数组,每个variant元素用std::in_place_index_t<Idx>初始化,然后使用std::visit访问第i和第j个元素,从而提取出类型信息进行比较。
std::type_identity在排序算法中起什么作用?
std::type_identity用作仿函数的参数类型,包裹实际类型T。这样做的原因是T可能是void、数组或不完整类型等不能直接作为函数参数的类型,而std::type_identity<T>可以安全地作为参数传递。
如何根据类型的大小对type_list进行排序?
定义一个仿函数,接受std::type_identity<T>参数并返回sizeof(T)。将该仿函数作为Key传递给Sort模板,即可按照类型的大小对type_list中的类型进行排序。例如,Sort<type_list<double,int,char>, Key>::type 会得到 type_list<char,int,double>。
排序算法中的lambda为什么必须是constexpr的?
因为整个排序过程需要在编译期完成,lambda作为比较器在std::ranges::sort中使用,而std::ranges::sort在constexpr上下文中调用,所以lambda必须声明为constexpr,以便在编译时求值。
这种基于索引映射的排序方法适用于哪些场景?
适用于模板元编程中需要对type_list中的类型进行排序的场景,例如按类型大小、对齐要求或其他编译期属性排序。该方法利用C++20 ranges算法和constexpr特性,简化了传统模板元编程的复杂度。