【问题标题】:C++ Iterators for multi-dimensional C arrays用于多维 C 数组的 C++ 迭代器
【发布时间】:2014-04-20 20:41:16
【问题描述】:

我有大量需要迭代的 3 到 6 维 C 数组。像 boost::multi_array 这样的更多 C++'y 表示不是一个选项,因为这些数组来自 C 框架 PETSc(使用 fortran 排序,因此向后索引)。简单的循环最终看起来像这样:

  for (int i=range.ibeg; i<=range.iend; ++i){
   for (int j=range.jbeg; j<=range.jend; ++j){
     for (int k=range.kbeg; k<=range.kend; ++k){
       (...)

甚至更糟:

  for (int i=range.ibeg-1; i<=range.iend+1; ++i){
    for (int j=range.jbeg-1; j<=range.jend+1; ++j){
      for (int k=range.kbeg-1; k<=range.kend+1; ++k){
       for (int ii=0; ii<Np1d; ++ii){
        for (int jj=0; jj<Np1d; ++jj){
         for (int kk=0; kk<Np1d; ++kk){
           data[k][j][i].member[kk][jj][ii] = 
            func(otherdata[k][j][i].member[kk][jj][ii],
                 otherdata[k][j][i].member[kk][jj][ii+1]);

有很多这样的实例,循环索引的范围不同,这一切都变得非常丑陋并且可能容易出错。应该如何为这样的多维数组构造迭代器?

【问题讨论】:

  • 光是看着就让人不寒而栗……
  • 你和我都是,但这就是我生活的世界。
  • 递归?在某些情况下,递归更胜一筹。如果你将它与模板结合起来,你甚至可以内联所有内容。
  • 能举个代码例子吗?
  • 你能把 Foo[X][Y][Z] 当作 Foo[XYZ] 对待吗?然后 foo[i][j][k] 变为 [i+jX+kX*Y]。或者这只是碰巧起作用并且不受标准保证的事情?

标签: c++ multidimensional-array iterator boost-iterators


【解决方案1】:

毕竟,完全模板化的版本并没有那么难,所以这里有一个单独的答案,同样是live example。如果我没记错的话,这应该在自定义嵌套循环之上具有零开销。你可以测量并告诉我。无论如何,我打算为自己的目的实现它,这就是我在这里付出努力的原因。

template<size_t N>
using size = std::integral_constant<size_t, N>;

template<typename T, size_t N>
class counter : std::array<T, N>
{
    using A = std::array<T, N>;
    A b, e;

    template<size_t I = 0>
    void inc(size<I> = size<I>())
    {
        if (++_<I>() != std::get<I>(e))
            return;

        _<I>() = std::get<I>(b);
        inc(size<I+1>());
    }

    void inc(size<N-1>) { ++_<N-1>(); }

public:
    counter(const A& b, const A& e) : A(b), b(b), e(e) { }

    counter& operator++() { return inc(), *this; }

    operator bool() const { return _<N-1>() != std::get<N-1>(e); }

    template<size_t I>
    T& _() { return std::get <I>(*this); }

    template<size_t I>
    constexpr const T& _() const { return std::get <I>(*this); }
};

我现在有 _ 方法而不是 operator[](可以随意重命名),它只是 std::get 的快捷方式,所以用法并不比 operator[] 更冗长:

    for (counter<int, N> c(begin, end); c; ++c)
        cout << c._<0>() << " " << c._<1>() << " " << c._<2>() << endl;

其实你可以试试以前的版本

    for (counter<int, N> c(begin, end); c; ++c)
        cout << c[0] << " " << c[1] << " " << c[2] << endl;

并测量,因为它可能是等价的。为此,请将std::array 继承切换为public 或在counterpublic 部分中声明using A::operator[];

绝对不同的是operator++,它现在基于递归模板函数inc(),而有问题的条件if (n &lt; N - 1) 被替换为没有开销的专门化(实际上是重载)。

如果最终证明存在开销,最终的尝试是将std::array 替换为std::tuple。在这种情况下,std::get 是唯一的方法;没有operator[] 替代方案。 T 类型重复 N 次也会很奇怪。但我希望这不是必需的。

进一步的概括是可能的,例如指定每个维度的(编译时)增量步骤,甚至指定每个维度的任意间接数组,例如模拟

a([3 5 0 -2 7], -4:2:20)

在类似 Matlab 的语法中。

但这需要更多的工作,如果你喜欢这种方法,我认为你可以从这里开始。

【讨论】:

  • 太棒了,会试试这个然后回来。这适用于任意 n 维向量是吗?
  • 它适用于 1 维数组、n 维数组、自定义类、函数、表达式或几乎任何你放在单个 for 循环块中的任何组合。它不保存任何数据,它只是计算并为您提供坐标c[0], c[1], ...,您可以随意使用。
  • @Aurelius 当然,一个要求是随机访问底层数组。否则,您将需要计数器/迭代器来保存对数据的引用并增加底层迭代器而不是整数坐标。这是另一种概括。
  • 出色的答案。使用调试编译此解决方案比 C 循环慢约 10%,并且与 -O3 编译相同。这极大地清理了我的代码。如果您接受啤酒作为付款,请给我一个地址;)
【解决方案2】:

在嵌套 for 循环的简单情况下,不需要成熟的 n 维迭代器。由于只需要一次遍历,所以一个简单的计数器就足够了,可以像这样轻松定制:

template<typename T, size_t N>
class counter
{
    using A = std::array<T, N>;
    A b, i, e;

public:
    counter(const A& b, const A& e) : b(b), i(b), e(e) { }

    counter& operator++()
    {
        for (size_t n = 0; n < N; ++n)
        {
            if (++i[n] == e[n])
            {
                if (n < N - 1)
                    i[n] = b[n];
            }
            else
                break;
        }

        return *this;
    }

    operator bool() { return i[N - 1] != e[N - 1]; }

    T&       operator[](size_t n)       { return i[n]; }
    const T& operator[](size_t n) const { return i[n]; }
};

这样使用这个计数器就很容易了:

int main()
{
    constexpr size_t N = 3;
    using A = std::array<int, N>;

    A begin = {{0, -1,  0}};
    A end   = {{3,  1,  4}};

    for (counter<int, N> c(begin, end); c; ++c)
        cout << c << endl;
        // or, cout << c[0] << " " << c[1] << " " << c[3] << endl;
}

假设counter 有一个运算符&lt;&lt;。完整代码见live example

最里面的条件if (n &lt; N - 1) 说明能够检查终止并且总是检查不是那么有效。对我来说,如何分解它并不是很明显,但无论如何它只会在我们前进到计数器的下一个“数字”时发生,而不是在每次递增操作时发生。

如果counter 派生std::array 而不是使用c[0], c[1], c[2] 等,则使用std::get 比使用i 更有效(而b,e 仍然是成员)。这个想法可以扩展到operator++operator bool)的编译时递归实现,这将消除其中的for循环,以及上面讨论的有问题的检查。在这种情况下,operator[] 将被丢弃。但这一切都会使counter 代码更加晦涩难懂,我只是想强调一下这个想法。它还会使counter 的使用更加冗长,但这是您需要为效率付出的代价。

当然,可以通过使用更多方法和特征扩展counter 来构建成熟的 n 维迭代器。但要使其足够通用可能是一项艰巨的任务。

【讨论】:

  • 非常简洁的解决方案,我会在星期一解决这个问题然后回来。您可以编辑 std::get 的示例吗?这是性能关键代码; 100 多个内核上的多天运行时间,即使是最好的实现也是如此,因此任何比 C 样式循环慢得多的东西都是不可行的。
  • 我最初误解了......它绝对需要是一个“成熟的 n 维”(或 3,5 和 6-D)迭代器,因为我们没有遍历所有数组数据,但仅限于其中的有限部分。例如,数据可能在数组 a[nk][nj][ni] 中,有些函数需要遍历所有的数组,但大多数函数会在某个子集上操作,例如 a[1:nk-1][0:新泽西州][0]
  • 嗯,好的,我明白了。是的,完全模板化的代码确实可以根据需要采取零开销,但需要做更多的工作:-) 我会尝试的。与此同时,你可以试试这个并测量一下。
  • 谢谢,它已经让我对如何前进有了一个好主意。具有讽刺意味的是,这种类型的事情在 Fortran 中非常简单,只需编写 a(ibeg:iend,jbeg:jend,kbeg:kend) = ...
  • @Aurelius 这不是问题。 “成熟的”迭代器是指双向或随机访问、类型特征等。您在此处所说的内容可以通过beginend 数组的适当初始化来定制。
猜你喜欢
  • 2013-01-23
  • 2015-04-24
  • 2015-10-05
  • 2014-03-27
  • 1970-01-01
  • 2023-03-08
  • 2014-05-17
  • 1970-01-01
相关资源
最近更新 更多