У меня есть большое количество 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]);
Существует много подобных примеров с различными диапазонами индексов цикла, и все это становится очень уродливым и потенциально подверженным ошибкам. Как построить итераторы для таких многомерных массивов?
Полностью шаблонная версия, в конце концов, не была такой сложной, так что здесь это в отдельном ответе, опять же с живой пример. Если я не ошибаюсь, это должно иметь нулевые накладные расходы поверх пользовательских вложенных циклов. Вы можете измерить и дать мне знать. В любом случае я намерен реализовать это в своих целях, поэтому я приложил все усилия здесь.
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-подобном синтаксисе.
Но это требует еще больше работы, и я думаю, что вы можете взять это отсюда, если вам нравится подход.
Полноценный 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
с большим количеством методов и черт. Но сделать его достаточно общим может быть огромным предприятием.