T231111 Compile time for
先说明,本篇并不是说 Expansion statements。
遍历方式分为两种,自下而上叫迭代,自上而下叫递归。
递归往往需要多个函数(if constexpr
可以避免),一般迭代是一种更加自然的方式。前面正式文章写过大量 tuple-like
类型的遍历方法,虽然也包含了非 tuple-like
类型的迭代方法,但却是作为次要部分出现的,这篇单独摘出来再讲解一下。
tuple-like
类型就是指存在 std::tuple_size<T>
和 std::get<I>(t)
定义的类型,此类类型具备通用的遍历方式。由旧文可知,目前最简单的迭代方式是借助 std::apply
:
void pack_iterator_with_tuple(auto&&... args) {
auto tuple = std::make_tuple(std::forward<decltype(args)>(args)...);
std::apply([](const auto&... tupleArgs) {
((std::cout << tupleArgs << " "), ...);
}, tuple);
}
当然,对于参数包来说,最简单的方式是借助 Fold expressions。
void pack_iterator_with_fold_expr(auto&&... args) {
((std::cout << std::forward<decltype(args)>(args) << ' '), ...);
}
Fold expressions 也扩展讲过很多高级技巧,它是 Language 级别的特性,而 std::apply
是 Library 级别的特性,后者其实可以基于前者而实现。比如我们之前展现过的一种 any_visitor
实现:
// Implement any visitor using fold expressions
template <class... Ts>
void any_visit(auto f, const std::any& a) {
((std::type_index(a.type()) == std::type_index(typeid(Ts))
&& (f(std::any_cast<Ts>(a)), true)) || ...);
}
std::vector<std::any> container { 5, 0.42, "hello", false };
// Output: 5 0.42 hello boolean: false
std::ranges::for_each(container, [](const auto& a) {
any_visit<int, double, const char*>([](const auto& x) { fmt::print("{} ", x); }, a);
any_visit<bool>([](const auto& x) { fmt::print("boolean: {} ", x); }, a);
});
这种迭代方式与 Range-based for loop
的思想本质上没有什么不同,都是只集中于元素的局部。迭代之时并不会有什么问题,因为那时总是只需关注局部信息,而某些时候,却可能需要从一个整体角度对元素进行操作,此时便需要回归到最原始的 for
,它有索引下标,不仅可以操作当前元素,还可以操作任何其它位置的元素。
举个例子,先回到本文开头的示例:
void pack_iterator_with_tuple(auto&&... args) {
auto tuple = std::make_tuple(std::forward<decltype(args)>(args)...);
std::apply([](const auto&... tupleArgs) {
((std::cout << tupleArgs << " "), ...);
}, tuple);
}
此时你只是访问当前的局部元素,当然只需使用 std::apply
就可以实现。然而,若是需要将其功能改为逆序元素,此时就无法再使用这种迭代方式,因为它无法进行整体性操作。
此时,最简单的方式就是回到索引时代,以下是一种实现方式:
void reverse_of(auto... args) {
auto tuple = std::make_tuple(std::forward<decltype(args)>(args)...);
return [&tuple]<auto... I>(std::index_sequence<I...>) {
return std::make_tuple(std::get<sizeof...(args) - 1 - I>(tuple)...);
}(std::index_sequence_for<decltype(args)...>{});
}
可以直接把 []<auto... I>(std::index_sequence<I...>) {}(std::make_index_sequence<N>{})
看作是一种编译期的原始 for
循环,它们都有索引范围,每次向前迭代,思想上没有什么本质区别。
std::integer_sequence
本身并非是 tuple-like
类型,所以并不支持 std::apply
,但结合 IIFE, 它就是一种编译期的迭代方式,只是写法丑一些而已。
编译期与运行期编程的核心思想其实大差不大,只是语法表达形式上存在差异,一旦理解这种思想本质,编译期的很多代码看起来其实并不复杂。
本部分未完,下篇继续……