带模板的 N 维嵌套元循环

N-dimensionally nested metaloops with templates

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

一个简单的未嵌套的 metaloop 如下所示:

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;
}

现在,我想制作一个 metaloop,它接受一个表示维度(嵌套级别)的 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;
}

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

Live Demo

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

它在具有相同起点和终点的 N-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> 的专业化。第二个位置指示外循环的当前索引,最后一个位置指示要迭代到(但不包括)的索引。所以在这种情况下,当前索引与上一个索引相同,表明我们已完成循环(因此 "do nothing" 函数)。

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

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 这是一个可变参数模板。

内循环

当我说 "inner loop" 时,我指的是最内层的循环。这个循环需要简单地递增,直到开始索引到达结束索引,而不是尝试在任何较低的维度上递归。在我们的例子中,这是当我们的 Dim(维度)参数为 1:

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

此时,我们终于要调用我们传递的函数,连同我们迄今为止积累的所有参数(外循环的索引)加上最内层循环的索引:

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>
{/*...*/};

因此这里有 "do nothing" 功能;不应执行任何工作,因为循环正在终止。

最后,我加入了最后一个专业化以捕获用户未指定任何维度的错误:

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!");

结论

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

我最喜欢这段代码的应用是我们可以用它来制作一个 N-dimensional 计数器。例如,N-bits 的二进制计数器(在现场演示中找到)。

由于这个问题似乎仍然很受欢迎,我认为最好展示一下在 C++17 中这样做是多么容易。一、完整代码

Demo

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,我们只需在循环中用下一个索引调用 provided-lambda
  2. 否则,我们从提供的可调用对象创建一个新的可调用对象,只是我们将循环索引绑定到可调用参数之一。然后我们用少 1 个维度递归我们的元 for 循环。

如果您完全熟悉函数式编程,这会更容易理解,因为它是 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,因为我们正在对 half-closed 区间 [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 是可变的。这在我们的 meta for 循环中很有用,因为我们想要模拟这样一个事实,即外循环保持在同一索引处,而内循环遍历其整个范围。 例如

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

i == 0我们遍历j的所有值从0M,然后我们重复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 if
  5. if 分支
  6. 我们用 i = 1 调用 bind_an_argument,它用参数 (0, 0) 从上面调用我们的 callable,第一个参数是从之前对 [= 的调用绑定的16=]。这会产生输出

    0 0

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

    0 1

  8. 我们完成迭代,因此堆栈展开到 parent 调用函数
  9. 我们以 Dimensions == 2Callable == callable 返回 meta_for_loop 的电话会议。我们完成了第一次循环迭代,然后将 i 增加到 1
  10. Dimensions == 2开始,我们再次进入else分支
  11. 重复步骤 4 到 10,除了 callable 的第一个参数绑定到 1 而不是 0。这会产生输出

    1 0
    1 1