具有模板的N维嵌套metaloops

pla*_*cel 9 c++ templates metaprogramming template-meta-programming c++11

我正在尝试使用模板元编程来进行N维嵌套的metaloops.嵌套部分是微不足道的,但是将所有任意数量的迭代索引作为模板参数传递到最内部循环似乎是有问题的.

一个简单的unnested 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;
}
Run Code Online (Sandbox Code Playgroud)

现在,我想创建一个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;
}
Run Code Online (Sandbox Code Playgroud)

And*_*dyG 5

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

现场演示

我的解决方案的要点是声明N个维度,带有一个起点和一个终点。

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

当到达第一维时,它将实际上开始增加起始位置,并调用传递的函数。

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

所以这样的电话:

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

将产生如下输出:

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));
    }
};
Run Code Online (Sandbox Code Playgroud)

和助手:

// 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!");
};
Run Code Online (Sandbox Code Playgroud)

更多说明

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

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

template<int Dim, size_t B, size_t E> 
struct iterate<Dim, E, B, E>
{ /*..*/};
Run Code Online (Sandbox Code Playgroud)

请注意,这是的专业化<Dim, E, B, E>。第二个位置指示外循环的当前索引,最后一个位置指示要迭代到(但不包括)的索引。因此,在这种情况下,当前索引与最后一个索引相同,表示我们已完成循环(因此执行“不执行任何操作”功能)。

外循环的递归情况涉及以下情况:循环索引小于要迭代的索引。用模板术语来说,第二个位置小于第四个位置:

template<int Dim, size_t S, size_t B, size_t E>
struct iterate
{/*...*/}
Run Code Online (Sandbox Code Playgroud)

请注意,这不是专业。

此函数的逻辑是,外循环应发信号通知内循环从其开始就开始执行,然后外循环继续并为内循环重新开始该过程:

iterate<Dim-1, B, B, E>::apply (func, a..., S);
iterate<Dim, S+1, B, E>::apply (func, a...);
Run Code Online (Sandbox Code Playgroud)

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

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

::apply(func, a..., S)
Run Code Online (Sandbox Code Playgroud)

正在传递该函数以及更高维度的循环的索引,然后附加当前循环的索引(S)。a这是一个可变参数模板。

内循环

当我说“内部循环”时,是指最内部的循环。该循环只需简单地增加,直到开始索引到达结束索引为止,而不要尝试在任何较低维度上递归。在我们的例子中,这是我们的Dim(Dimension)参数为1的情况:

template<size_t S, size_t B, size_t E>
struct iterate<1, S, B, E>
{/*...*/};
Run Code Online (Sandbox Code Playgroud)

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

func(a..., B);
Run Code Online (Sandbox Code Playgroud)

然后递归(增量索引)

iterate<1, S, B+1, E>::apply(func, a...);
Run Code Online (Sandbox Code Playgroud)

最基本的情况是最内层循环的索引与结束索引相同(且维数为1):

template<size_t S, size_t E>
struct iterate<1, S, E, E>
{/*...*/};
Run Code Online (Sandbox Code Playgroud)

因此,这里的“不执行任何操作”功能;因为循环正在终止,所以不应执行任何工作。

最后,我提供了最后一个专业化功能,以捕获未指定任何尺寸的用户错误:

template<size_t S, size_t B, size_t E>
struct iterate<0, S, B, E>
Run Code Online (Sandbox Code Playgroud)

哪个static_assert总是因为sizeof(size_t)不为零而失败:

static_assert(sizeof(S) == 0, "Need more than 0 dimensions!");
Run Code Online (Sandbox Code Playgroud)

结论

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

该代码最喜欢的应用是我们可以使用它制作一个N维计数器。例如,一个用于N位的二进制计数器(在实时演示中找到)。


And*_*dyG 5

由于这个问题似乎仍在增加流量,我认为这是一个很好的主意,以表明在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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

说明:

  1. 如果Dimensions为1,我们只需在循环中使用下一个索引调用提供的lambda
  2. 否则,我们从提供的可调用对象中创建一个新的可调用对象,除了将循环索引绑定到其中一个可调用参数之外。然后,我们在meta 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;
};
Run Code Online (Sandbox Code Playgroud)

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

我们将上述定义的meta for循环称为:

meta_for_loop<2>(0, 2, callable);
Run Code Online (Sandbox Code Playgroud)

该end给的说法meta_for_loop是2,而不是1,因为我们正在建模一个半封闭区间[开始,结束),这是常见的编程,因为人们往往希望第一个指标被列入他们的循环,然后他们要迭代(完-开始)次。

让我们逐步介绍一下算法:

  1. Dimensions == 2,所以我们不会使我们的静态断言失败
  2. 我们开始迭代, i = 0
  3. Dimensions == 2,因此我们进入constexpr if语句 的“ else”分支
    • 我们创建一个新的callable,它捕获传入的callable并将其命名bind_an_argument为反映我们正在绑定提供的callable的一个参数c。

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

void bind_an_argument(size_t j)
{
    c(i, j);
}
Run Code Online (Sandbox Code Playgroud)

请注意,i保持不变,但是j是可变的。这在我们的meta for循环中很有用,因为我们要对以下事实建模:外循环保持相同的索引,而内循环在整个范围内迭代。例如

for(int i = 0; i < N; ++i)
{
    for (int j = 0; j < M; ++j)
    {
       /*...*/
    }
}
Run Code Online (Sandbox Code Playgroud)

当i == 0我们遍历过的所有值j从0到M,然后我们重复了i == 1,i == 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所以我们进入我们的if分支constexpr if
  5. 我们bind_an_argument使用with i = 1进行调用,后者callable从上方调用带有参数的参数(0, 0),其中第一个与上一次调用绑定meta_for_loop。这产生输出

    0 0

  6. 我们bind_an_argument使用with i == 1进行调用,后者callable从上方调用参数(0, 1),在我们之前调用时绑定了第一个参数meta_for_loop。这产生输出

    0 1

  7. 我们完成了迭代,因此堆栈退回到了父调用函数
  8. 我们回到了meta_for_loop与Dimensions == 2和的通话中Callable == callable。我们完成第一个循环迭代,然后递增i到1
  9. 由于Dimensions == 2,我们else再次进入分支
  10. 重复第4步到第10步,除了第一个参数to callable绑定到1而不是上0。这产生输出

    1 0
    1 1