先说明,本篇并不是说 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::applyLibrary 级别的特性,后者其实可以基于前者而实现。比如我们之前展现过的一种 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, 它就是一种编译期的迭代方式,只是写法丑一些而已。

编译期与运行期编程的核心思想其实大差不大,只是语法表达形式上存在差异,一旦理解这种思想本质,编译期的很多代码看起来其实并不复杂。

本部分未完,下篇继续……

Leave a Reply

Your email address will not be published. Required fields are marked *

You can use the Markdown in the comment form.