使用用户定义函数展开泛型循环

generic loop-unrolling with user-defined function

本文关键字:泛型 循环 函数 用户 定义      更新时间:2023-10-16

最近我决定制作循环展开器。我的函数从_Beg迭代到_End(这是模板参数),并在每个索引上调用函数_func:

template<size_t _Beg, size_t _End, typename _Func>
typename std::enable_if<_Beg < _End, void>::type
for_r(_Func _func)
{
_func(_Beg);
for_r<_Beg+1, _End>(_func);
}
template<size_t _Beg, size_t _End, typename _Func>
typename std::enable_if<_Beg >= _End, void>::type
for_r(_Func _func)
{}

它可以这样工作:

for_r<0, 10>([](size_t index){cout << index << endl;});

但是,"index"变量在编译时是已知的,因此从逻辑上讲,可以在lambda中使用"index"作为常量表达式。像这样:

tuple<int, int, int, int> tpl(1, 2, 3, 4);
for_r<0, 4>([&](size_t index){cout << get<index>(tpl) << endl;});

但是"index"是可变的,不可能将其作为constexpr传递到lambda中。有没有办法在不显式键入循环展开的情况下处理它并实现逻辑行为:

cout << get<0>(tpl) << endl << get<1>(tpl) << endl << get<2>(tpl) << endl << get<3>(tpl) << endl;

由于7年过去了,现在我们有了很好的功能,如C++17的折叠表达式和C++20的显式模板化lambdas,现在可以编写和使用这样的"constexpr表示"-喜欢事物很容易。例如:

// Offsets std::integer_sequence by OFFSET
template <typename T, T OFFSET, T... IDXS>
constexpr std::integer_sequence<T, OFFSET + IDXS...> offset(
std::integer_sequence<T, IDXS...>)
{
return {};
}
// Calls templated operator() of given function object
// with INDEX template parameter from range [START; END)
template<std::integral T, T START, T END>
void for_constexpr_interval(auto iteration)
{
if constexpr (START >= END)
return;
[&]<T... IDXS>(std::integer_sequence<T, IDXS...>) {
(iteration.template operator()<IDXS>(), ...);
}(offset<T, START>(std::make_integer_sequence<T, END - START>()));
}

for_constexpr_interval中,我们使用显式模板化的立即调用lambda,通过从std::integer_sequence参数(在std::make_integer_sequence之上的offset函数的帮助下创建,以表示[START; END)间隔)推导引入模板参数包IDXS,同时避免不必要的辅助函数污染命名空间。这个包现在可以直接由fold表达式使用,而不需要使用递归实例化。在该fold表达式中,我们使用适当的模板参数调用iteration函数对象的模板化operator()。需要语法.template operator()来消除operator()表示模板的歧义,使得例如后面的<不被解释为";小于";

它可以这样使用:

std::tuple tpl(1, 2, 3, 4);
for_constexpr_interval<size_t, 0, 4>([&]<size_t INDEX>() {
if constexpr (INDEX > 0)
std::cout << ' ';
std::cout << std::get<INDEX>(tpl);
});
std::cout << std::endl;
for_constexpr_interval<int, -1, 2>([&]<int INDEX>() {
std::cout << INDEX << std::endl;
});

Godbolt

请注意,当将函数对象传递给for_constexpr_interval时,我们再次使用显式模板化的lambda。此外,从生成的程序集中可以看出,即使在-O1main中的优化调用也会内联,这至少在最新的GCC、Clang和MSVC中会发生。如果需要,编译器特定的属性/说明符可以强制内联(在for_constexpr_interval、内部立即调用的lambda、传递的lambda的任何组合上),尽管通常这样的决策最好留给编译器。

最后,for_constexpr_interval可以进一步更改/广义/补充,例如,通过允许索引、STEP(增量)参数或非std::integral索引类型的反向遍历,但这将需要更复杂的实现来在内部lambda的参数中创建索引序列(可能会放弃std::make_integer_sequence以进行自定义替换),并对浮点索引进行一些维护。