【问题标题】:N-dimensionally nested metaloops with templates带有模板的 N 维嵌套金属循环
【发布时间】:2016-04-04 19:09:22
【问题描述】:

我正在尝试使用模板元编程来进行 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;
}

【问题讨论】:

  • [OT]:更喜欢 std::forward&lt;Lambda&gt; 而不是 static_cast&lt;Lambda&amp;&amp;&gt;
  • @Jarod42:不,我讨厌 std::forward 有多种原因。 1:实际上它执行一个演员,但它的名字并没有反映这一点。 2:如果使用 std::forward 而不是普通转换,某些编译器(如 MSVC 14)会生成附加指令和/或无法对 lambda 执行优化。它可能会导致严重的性能问题。也许这是一个错误,也许不是,简单的演员表对我来说更加清晰和安全。我还为它制作了一个自定义的类似 C++11 的 forward_cast 函数。
  • 在您的示例中,您可以从0..10*10*10 线性化您的索引,并用模数重新计算每个部分。见Completely enumerate indices of D-dimensional array at compile time
  • 好吧,我想这不是重复的,但无论如何我用“metaloops”做了一次非常相似的事情,我认为我的模板代码相当干净:stackoverflow.com/questions/32320753/…
  • 所以,对于一些解释,我的想法是,而不是试图完全模仿 N 嵌套循环的语义,我只是实现任意数量的类型列表的笛卡尔积,然后有一个笛卡尔积结果的单循环。对于运行时代码,这将需要更多内存,这不是一个好主意。但是对于编译时代码,我认为它并没有渐近不同,因为在编译时,无论如何,您都需要为任何循环计算的每次迭代实例化一个新类型/函数/某些东西。在这里,无论如何你都在输出那个大小的结构。

标签: c++ templates c++11 metaprogramming template-meta-programming


【解决方案1】:

由于这个问题似乎仍在吸引流量,我认为展示在 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,我们只需在循环中使用下一个索引调用提供的 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. Dimensions == 2,所以我们的静态断言不会失败
  2. 我们开始迭代,i = 0
  3. Dimensions == 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 时,我们将j 的所有值从0 迭代到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 &lt; 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

【讨论】:

  • 谢谢,这太棒了。
【解决方案2】:

更精通这方面的人可以改进我的答案。

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

请注意,这是&lt;Dim, E, B, E&gt; 的特化。第二个位置表示外部循环的当前索引,最后一个位置表示要迭代的索引(但不包括)。因此,在这种情况下,当前索引与上一个索引相同,表明我们已完成循环(因此是“无操作”函数)。

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

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 (Dimension) 参数为 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>
{/*...*/};

因此这里的“什么都不做”功能;由于循环正在终止,因此不应执行任何工作。

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

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 个嵌套的 for 循环,它们都具有相同的开始和结束索引,并且我们希望将这些索引传递给函数。我们可以做更多的工作来使iterate 结构可以独立存在,而无需假设外部循环的开始和结束索引与内部循环的相同。

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

【讨论】:

  • 我唯一不明白的是为什么 a... 的长度取决于类的第一个模板参数。您永远不会将实际值传递给可变参数模板参数。这是标准行为吗?通常 std::integer_sequence 用于这些目的。顺便说一句,一个很好的答案。
  • @plasmacel:我不完全理解你在问什么,但我会打赌并尽量做到彻底。参数包扩展a... 扩展为逗号分隔的参数列表。最初包是空的,但是随着模板被递归实例化,这个参数包增长到包括当前循环索引a..., S 在下一个递归级别变成a...,一直到a..., B。有效的递归导致包首先扩展为(&lt;none&gt;),然后是(S)(S, S+1)、...、(S, S+1, ..., E-1),或者具体来说是(0)(0, 1)、...、(0, 1, ..., 9)
  • 您在每次递归时传递相同数量的模板参数,所以我看不到它是如何增长的。我从未见过像这样的模板参数包扩展,但是它可以工作。
  • @placel:啊,现在我明白你在说什么了。我可能看起来传递了相同数量的模板参数,但实际上并非如此!正如我在之前的评论中提到的,每次进入较低级别的递归时,参数包都会增长一个值。当我调用iterate&lt;Dim-1, B, B, E&gt;::apply (func, a..., S); 时,我们在迭代时进入了较低级别的递归。在那个点,a 指的是递归的(a..., S)上一个 级别,现在实际上是(S-1)。然后我们再次递归a 接受(S-1, S-2, S) 有意义吗?
猜你喜欢
  • 1970-01-01
  • 2022-01-13
  • 2023-03-21
  • 2018-09-19
  • 1970-01-01
  • 2012-10-25
  • 2020-09-15
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多