带有模板的 N 维嵌套元循环

N-dimensionally nested metaloops with templates

本文关键字:嵌套 循环      更新时间:2023-10-16

我正在尝试使用模板元编程进行N维嵌套的元循环。嵌套部分很简单,但是将所有任意数量的迭代索引作为模板参数传递给最内部的循环似乎有问题。

一个简单的非嵌套元循环如下所示:

template <size_t I, size_t N>
struct meta_for
{
    template <typename Lambda>
    inline meta_for(Lambda &&iteration)
    {
        iteration(I);
        meta_for<I+1, N> next(static_cast<Lambda&&>(iteration));
    }
};
template <size_t N>
struct meta_for<N, N>
{
    template <typename Lambda>
    inline meta_for(Lambda &&iteration)
    {
        return;
    }
};
#include <iostream>
int main()
{
    meta_for<0, 10>([&](size_t i) // perform 10 iterations
    {
        std::cout << i << 'n';
    });
    return 0;
}

现在,我想创建一个元循环,它接受一个表示维度(嵌套级别(的 N 参数,使用如下:

#include <iostream>
int main()
{
    // perform 3 dimensionally nested iterations
    // each index goes from 0 to 10
    // so 10x10x10 iterations performed
    meta_for<3, 0, 10>([&](size_t i, size_t j, size_t k)
    {
        std::cout << i << ' ' << j << ' ' << k << 'n';
    });
    return 0;
}

由于这个问题似乎仍然有流量,我认为在 C++17 中展示这是多么容易是一个好主意。一、完整代码

演示

template<size_t Dimensions, class Callable>
constexpr void meta_for_loop(size_t begin, size_t end, Callable&& c)
{
    static_assert(Dimensions > 0);
    for(size_t i = begin; i != end; ++i)
    {
        if constexpr(Dimensions == 1)
        {
            c(i);
        }
        else
        {
            auto bind_an_argument = [i, &c](auto... args)
            {
                c(i, args...);
            };
            meta_for_loop<Dimensions-1>(begin, end, bind_an_argument);
        }
    }
}

解释:

  1. 如果维度为 1,我们只需调用提供的 lambda 并在循环中使用下一个索引
  2. 否则,我们从提供的可调用对象创建一个新的可调用对象,除了我们将循环索引绑定到其中一个可调用参数。然后我们在元 for 循环上递归,少了 1 维。

如果你完全熟悉函数式编程,这更容易理解,因为它是currying的应用程序。

它如何更具体地工作:

你想要一个二进制计数器

0

0
0 1
1 0
1 1

因此,您创建了一个可以打印两个整数的可调用对象,如下所示:

auto callable = [](size_t i, size_t j)
{
   std::cout << i << " " << j << std::endl;
};

由于我们有两列,所以我们有两个维度,所以 D = 2。

我们调用上面定义的元 for 循环,如下所示:

meta_for_loop<2>(0, 2, callable);

meta_for_loopend参数是 2 而不是 1,因为我们正在建模半闭区间 [start, end(,这在编程中很常见,因为人们通常希望第一个索引包含在他们的循环中,然后他们想要迭代(结束 - 开始(时间。

让我们逐步完成算法:

  1. 维度 == 2,所以我们不会失败我们的静态断言
  2. 我们开始迭代,i = 0
  3. 维度 == 2,所以我们输入 constexpr if 语句的 "else" 分支
    • 我们创建一个新的可调用对象,用于捕获传入的可调用对象并将其命名bind_an_argument以反映我们正在绑定所提供的可调用c的一个参数。

因此,bind_an_argument实际上看起来像这样:

void bind_an_argument(size_t j)
{
    c(i, j);
}

请注意,i保持不变,但j是可变的。这在我们的元 for 循环中很有用,因为我们希望模拟这样一个事实,即外部循环保持在同一索引,而内部循环迭代其整个范围。例如

for(int i = 0; i < N; ++i)
{
    for (int j = 0; j < M; ++j)
    {
       /*...*/
    }
}

i == 0我们从0迭代j的所有值到M,然后我们重复i == 1i == 2等。

  1. 我们再次调用meta_for_loop,只是Dimensions现在是1而不是2,我们的Callable现在是bind_an_argument而不是c
  2. Dimensions == 1,所以我们的static_assert通过
  3. 我们开始循环for(size_t i = 0; i < 2; ++i)
  4. Dimensions == 1,所以我们进入constexpr ifif分支
  5. 我们用 i = 1 调用bind_an_argument,它用参数(0, 0)从上面调用我们的callable,其中第一个是从上一个对meta_for_loop调用绑定的。这会产生输出
    0

    0

  6. 我们用i == 1调用bind_an_argument,它用参数(0, 1)从上面调用我们的callable,其第一个参数在我们之前调用meta_for_loop时绑定。这会产生输出

    0 1

  7. 我们完成迭代,因此堆栈展开到父调用函数
  8. 我们又回到了与Dimensions == 2Callable == callable meta_for_loop的呼吁中。我们完成第一次循环迭代,然后将i递增到1
  9. Dimensions == 2年起,我们再次进入else分支
  10. 重复步骤 4 到 10,不同之处在于要callable的第一个参数绑定到 1 而不是 0 。这会产生输出

    1 0
    1 1

更精通这些东西的人可以改善我的答案。

现场演示

我的解决方案的要点是你声明 N 个维度,有一个开始和一个结束。

它在具有相同开始和结束的 N-1 维度上递归。

当它到达第 1 维时,它实际上将开始递增开始,调用传递的函数。

它将始终尝试传递许多与维度数(其索引(相同的参数。

所以像这样的电话:

meta_for<2, 0, 2>::loop(
    [](size_t i, size_t j)
    {
        std::cout << i << " " << j << std::endl;
    });

将导致如下输出:

0

0

0 1

1 0

1

1

下面是使用帮助程序的meta_for结构,iterate

template<size_t D, size_t B, size_t E>
struct meta_for
{
    template<typename Func>
    static void loop(Func&& func)
    {
        iterate<D, B, B, E>::apply(std::forward<Func>(func));
    }
};

和帮手:

// a helper macro to avoid repeating myself too much
#define FN template<typename Func, typename... Args> 
             static void apply(Func&& func, Args&&... a)

// Outer loop. S="Self" or "Start". Indicating current index of outer loop. Intent is to iterate until S == E
template<int Dim, size_t S, size_t B, size_t E>
struct iterate
{
    static_assert(S < E && B < E, "Indices are wrong");
    FN
    {
        // outer loop recursive case. Recurse on lower Dimension (Dim-1), and then increment outer loop (S+1)
        iterate<Dim-1, B, B, E>::apply (func, a..., S);
        iterate<Dim, S+1, B, E>::apply (func, a...);
    }
};
// Outer loop base case
template<int Dim, size_t B, size_t E> 
struct iterate<Dim, E, B, E>
{
    FN
    {
        // outer loop base case, End == End. Terminate loop
    }
};
// innter loop. "S" is outer loop's current index, which we need to pass on to function
// "B" is inner loop's (this loop) current index, which needs to iterate until B == E
template<size_t S, size_t B, size_t E>
struct iterate<1, S, B, E>
{
    static_assert(S < E && B < E, "Indices are wrong");
    FN
    {
        // inner loop recursive case. Perform work, and then recurse on next index (B+1)
        func(a..., B);
        iterate<1, S, B+1, E>::apply(func, a...);
    }
};
// inner loop base case
template<size_t S, size_t E>
struct iterate<1, S, E, E>
{
    FN
    {
        // inner loop base case, End == End. Terminate loop
    }
};
// case where zero dimensions (no loop)
template<size_t S, size_t B, size_t E>
struct iterate<0, S, B, E>
{
    static_assert(sizeof(S) == 0, "Need more than 0 dimensions!");
};

更多解释

与任何其他涉及可变参数模板的解决方案一样,此解决方案依赖于递归。

我想在外循环上表达递归,所以我从一个基本情况开始;循环的结束。这是开始与结束相同的情况:

template<int Dim, size_t B, size_t E> 
struct iterate<Dim, E, B, E>
{ /*..*/};

请注意,这是 <Dim, E, B, E> 的专用化。第二个位置指示外部循环的当前索引,最后一个位置指示要迭代到(但不包括(的索引。因此,在这种情况下,当前索引与上一个索引相同,表明我们已经完成了循环(因此"什么都不做"函数(。

外部循环的

递归情况涉及循环索引小于要迭代到的索引的情况。在模板术语中,第二个位置小于第四个位置:

template<int Dim, size_t S, size_t B, size_t E>
struct iterate
{/*...*/}

请注意,这不是专业化。

此函数的逻辑是,外部循环应向内部循环发出信号以从其开始开始执行,然后外部循环继续并重新开始内部循环的进程:

iterate<Dim-1, B, B, E>::apply (func, a..., S);
iterate<Dim, S+1, B, E>::apply (func, a...);

请注意,在第一行中,第二个模板参数再次B,指示再次从头开始。这是必要的,因为第二行上的另一个递归情况递增S(递增外循环索引(。

在整个过程中,我们还在积累要传递给函数的参数:

::apply(func, a..., S)

将函数与更高维循环的索引一起传递,然后附加当前循环的索引 (S (。 a这是一个可变参数模板。

内环

当我说"内循环"时,我指的是最内在的循环。此循环只需递增,直到起始索引到达结束索引,而不是尝试在任何较低维度上递归。在我们的例子中,当我们的Dim(维度(参数为 1 时:

template<size_t S, size_t B, size_t E>
struct iterate<1, S, B, E>
{/*...*/};

在这一点上,我们终于想调用我们传递的函数,以及我们迄今为止积累的所有参数(外循环的索引(PLUS,最内层循环的索引:

func(a..., B);

然后递归(增量索引(

iterate<1, S, B+1, E>::apply(func, a...);

这里的基本情况是当最里面循环的索引与结束索引相同(维度为 1(时:

template<size_t S, size_t E>
struct iterate<1, S, E, E>
{/*...*/};

因此,这里的"什么都不做"函数;不应该执行任何工作,因为循环正在终止。

最后,我包括了最后一个专用化来捕获用户未指定任何维度的错误:

template<size_t S, size_t B, size_t E>
struct iterate<0, S, B, E>

它使用 static_assert 总是失败,因为sizeof(size_t)不为零:

static_assert(sizeof(S) == 0, "Need more than 0 dimensions!");

结论

这是一个特定的用例模板元程序。我们基本上为具有相同开始和结束索引的循环生成 N 个嵌套,并且我们希望将这些索引传递给函数。我们可以做更多的工作,使iterate结构可以独立存在,而无需假设外循环的开始和结束索引与内部循环的相同。

我最喜欢的代码应用是我们可以使用它来制作 N 维计数器。例如,N 位的二进制计数器(可在现场演示中找到(。