Итераторы C ++ для многомерных массивов C

У меня есть большое количество 3-6-мерных C-массивов, которые мне нужно перебрать. Больше представления C ++, такого как boost :: multi_array, не вариант, так как эти массивы поступают через платформу C PETSc (используя упорядочение по фортрану, отсюда обратное индексирование). Прямые циклы выглядят примерно так:

  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]);

Существует много подобных примеров с различными диапазонами индексов цикла, и все это становится очень уродливым и потенциально подверженным ошибкам. Как построить итераторы для таких многомерных массивов?

3

Решение

Полностью шаблонная версия, в конце концов, не была такой сложной, так что здесь это в отдельном ответе, опять же с живой пример. Если я не ошибаюсь, это должно иметь нулевые накладные расходы поверх пользовательских вложенных циклов. Вы можете измерить и дать мне знать. В любом случае я намерен реализовать это в своих целях, поэтому я приложил все усилия здесь.

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 или объявить using A::operator[]; в counter«s public раздел.

Что определенно отличается operator++, который теперь основан на рекурсивной функции шаблона inc() и проблемное состояние if (n < N - 1) заменяется специализацией (фактически, перегрузкой), которая не имеет накладных расходов.

Если окажется, что в конечном итоге накладные расходы, окончательная попытка будет заменить std::array от std::tuple, В этом случае, std::get это единственный путь; здесь нет operator[] альтернатива. Также будет странно, что тип T повторяется N раз. Но я надеюсь, что это не понадобится.

Возможны дальнейшие обобщения, например, указание шага (времени компиляции) для каждого измерения или даже указание произвольных косвенных массивов для измерения, например, симулировать

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

в Matlab-подобном синтаксисе.

Но это требует еще больше работы, и я думаю, что вы можете взять это отсюда, если вам нравится подход.

1

Другие решения

Полноценный n-мерный итератор не нужен в вашем простом случае вложенного for петли. Так как необходим только один обход, достаточно простого счетчика, который легко сделать на заказ, например:

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, Увидеть живой пример для полного кода.

Внутреннее состояние if (n < N - 1) учитывает возможность проверки на прекращение и не всегда эффективен для проверки. Для меня не было так очевидно, как это вычислить, но в любом случае это происходит только тогда, когда мы продвигаемся к следующей «цифре» счетчика, а не при каждой операции приращения.

Вместо того, чтобы использовать c[0], c[1], c[2] и т. д., более эффективно использовать std::get если counter производный std::array вместо того, чтобы иметь члена i (в то время как b,e остаются членами). Эту идею можно распространить на рекурсивную реализацию времени компиляции operator++ (operator bool а) что бы устранить for цикл внутри него, вместе с проблемной проверкой, обсужденной выше. operator[] будет отброшен в этом случае. Но все это сделало бы counter код более неясен, и я просто хотел выделить идею. Было бы также использовать counter немного более многословно, но это цена, которую вы должны заплатить за эффективность.

Конечно, полноценный n-мерный итератор может быть построен путем расширения counter с большим количеством методов и черт. Но сделать его достаточно общим может быть огромным предприятием.

3