Подтвердить что ты не робот

Чистые способы записи нескольких циклов 'for'

Для массива с несколькими измерениями обычно нужно написать цикл for для каждого из своих измерений. Например:

vector< vector< vector<int> > > A;

for (int k=0; k<A.size(); k++)
{
    for (int i=0; i<A[k].size(); i++)
    {
        for (int j=0; j<A[k][i].size(); j++)
        {
            do_something_on_A(A[k][i][j]);
        }
    }
}

double B[10][8][5];
for (int k=0; k<10; k++)
{
    for (int i=0; i<8; i++)
    {
        for (int j=0; j<5; j++)
        {
            do_something_on_B(B[k][i][j]);
        }
    }
}

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

4b9b3361

Ответ 1

Во-первых, вы не используете такую ​​структуру данных. Если вам нужна трехмерная матрица, вы определяете один:

class Matrix3D
{
    int x;
    int y;
    int z;
    std::vector<int> myData;
public:
    //  ...
    int& operator()( int i, int j, int k )
    {
        return myData[ ((i * y) + j) * z + k ];
    }
};

Или, если вы хотите индексировать, используя [][][], вам понадобится operator[] который возвращает прокси.

Как только вы это сделаете, если вы обнаружите, что вам постоянно приходится итерации, как вы представили, вы выставляете итератор, который будет поддержите его:

class Matrix3D
{
    //  as above...
    typedef std::vector<int>::iterator iterator;
    iterator begin() { return myData.begin(); }
    iterator end()   { return myData.end();   }
};

Затем вы просто пишете:

for ( Matrix3D::iterator iter = m.begin(); iter != m.end(); ++ iter ) {
    //  ...
}

(или просто:

for ( auto& elem: m ) {
}

если у вас есть С++ 11.)

И если вам нужны три индекса во время таких итераций, возможно создать итератор, который их предоставляет:

class Matrix3D
{
    //  ...
    class iterator : private std::vector<int>::iterator
    {
        Matrix3D const* owner;
    public:
        iterator( Matrix3D const* owner,
                  std::vector<int>::iterator iter )
            : std::vector<int>::iterator( iter )
            , owner( owner )
        {
        }
        using std::vector<int>::iterator::operator++;
        //  and so on for all of the iterator operations...
        int i() const
        {
            ((*this) -  owner->myData.begin()) / (owner->y * owner->z);
        }
        //  ...
    };
};

Ответ 2

Использование макроса для скрытия контуров for может быть очень запутанным, просто чтобы сохранить несколько символов. Вместо этого я использовал бы диапазон для:

for (auto& k : A)
    for (auto& i : k)
        for (auto& j : i)
            do_something_on_A(j);

Конечно, вы можете заменить auto& на const auto&, если вы, по сути, не изменяете данные.

Ответ 3

Что-то вроде этого может помочь:

 template <typename Container, typename Function>
 void for_each3d(const Container &container, Function function)
 {
     for (const auto &i: container)
         for (const auto &j: i)
             for (const auto &k: j)
                 function(k);
 }

 int main()
 {
     vector< vector< vector<int> > > A;     
     for_each3d(A, [](int i){ std::cout << i << std::endl; });

     double B[10][8][5] = { /* ... */ };
     for_each3d(B, [](double i){ std::cout << i << std::endl; });
 }

Чтобы сделать его N-арным, нам нужна магия шаблона. Прежде всего, мы должны создать структуру SFINAE, чтобы отличить это значение или контейнер. Реализация по умолчанию для значений и специализации для массивов и каждого из типов контейнеров. Как отмечает @Zeta, мы можем определить стандартные контейнеры по вложенному типу iterator (в идеале мы должны проверить, может ли тип использоваться с базой-основанием for или нет).

 template <typename T>
 struct has_iterator
 {
     template <typename C>
     constexpr static std::true_type test(typename C::iterator *);

     template <typename>
     constexpr static std::false_type test(...);

     constexpr static bool value = std::is_same<
         std::true_type, decltype(test<typename std::remove_reference<T>::type>(0))
     >::value;
 };

 template <typename T>
 struct is_container : has_iterator<T> {};

 template <typename T>
 struct is_container<T[]> : std::true_type {};

 template <typename T, std::size_t N>
 struct is_container<T[N]> : std::true_type {}; 

 template <class... Args>
 struct is_container<std::vector<Args...>> : std::true_type {};

Реализация for_each прост. Функция по умолчанию вызовет function:

 template <typename Value, typename Function>
 typename std::enable_if<!is_container<Value>::value, void>::type
 rfor_each(const Value &value, Function function)
 {
     function(value);
 }

И специализация будет называть себя рекурсивно:

 template <typename Container, typename Function>
 typename std::enable_if<is_container<Container>::value, void>::type
 rfor_each(const Container &container, Function function)
 {
     for (const auto &i: container)
         rfor_each(i, function);
 }

И вуаля:

 int main()
 {
     using namespace std;
     vector< vector< vector<int> > > A;
     A.resize(3, vector<vector<int> >(3, vector<int>(3, 5)));
     rfor_each(A, [](int i){ std::cout << i << ", "; });
     // 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,

     std::cout << std::endl;
     double B[3][3] = { { 1. } };
     rfor_each(B, [](double i){ std::cout << i << ", "; });
     // 1, 0, 0, 0, 0, 0, 0, 0, 0,
 }

Также это не будет работать для указателей (массивы, выделенные в куче).

Ответ 4

Большинство ответов просто демонстрируют, как С++ можно скрутить в непонятные синтаксические расширения, IMHO.

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

Если вы решили использовать необработанные данные, такие как 3-мерные массивы, просто жить с ним или определить класс, который дает некоторые понятные значения вашим данным.

for (auto& k : A)
for (auto& i : k)
for (auto& current_A : i)
    do_something_on_A(current_A);

просто согласуется с загадочным определением вектора вектора вектора int без явной семантики.

Ответ 5

#include "stdio.h"

#define FOR(i, from, to)    for(int i = from; i < to; ++i)
#define TRIPLE_FOR(i, j, k, i_from, i_to, j_from, j_to, k_from, k_to)   FOR(i, i_from, i_to) FOR(j, j_from, j_to) FOR(k, k_from, k_to)

int main()
{
    TRIPLE_FOR(i, j, k, 0, 3, 0, 4, 0, 2)
    {
        printf("i: %d, j: %d, k: %d\n", i, j, k);
    }
    return 0;
}

UPDATE: Я знаю, что вы просили об этом, но лучше не использовать это:)

Ответ 6

Одна идея состоит в том, чтобы написать итерируемый псевдокластерный класс, который "содержит" набор всех мультииндексных кортежей, которые вы индексируете. Никакой реализации здесь, потому что это займет слишком много времени, но идея в том, что вы должны иметь возможность писать...

multi_index mi (10, 8, 5);
  //  The pseudo-container whose iterators give {0,0,0}, {0,0,1}, ...

for (auto i : mi)
{
  //  In here, use i[0], i[1] and i[2] to access the three index values.
}

Ответ 7

Я предостерегаю этот ответ со следующим утверждением: это будет работать, только если вы работаете с фактическим массивом - он не будет работать для вашего примера, используя std::vector.

Если вы выполняете одну и ту же операцию над каждым элементом многомерного массива, не заботясь о позиции каждого элемента, вы можете воспользоваться тем фактом, что массивы размещены в смежных ячейках памяти и обрабатывают все как один большой одномерный массив. Например, если мы хотели бы умножить каждый элемент на 2.0 во втором примере:

double B[3][3][3];
// ... set the values somehow
double* begin = &B[0][0][0];     // get a pointer to the first element
double* const end = &B[3][0][0]; // get a (const) pointer past the last element
for (; end > begin; ++begin) {
    (*begin) *= 2.0;
}

Обратите внимание, что использование вышеуказанного подхода также позволяет использовать некоторые "правильные" методы на С++:

double do_something(double d) {
    return d * 2.0;
}

...

double B[3][3][3];
// ... set the values somehow
double* begin = &B[0][0][0];  // get a pointer to the first element
double* end = &B[3][0][0];    // get a pointer past the last element

std::transform(begin, end, begin, do_something);

Обычно я не рекомендую этот подход (предпочитая что-то вроде ответа Джефффри), поскольку он полагается на определенные размеры для ваших массивов, но в некоторых случаях это может быть полезно.

Ответ 8

Здесь я вижу много ответов, которые работают рекурсивно, обнаруживая, является ли вход контейнером или нет. Вместо этого, почему бы не определить, является ли текущий уровень тем же типом, что и функция? Это намного проще и позволяет использовать более мощные функции:

//This is roughly what we want for values
template<class input_type, class func_type> 
void rfor_each(input_type&& input, func_type&& func) 
{ func(input);}

//This is roughly what we want for containers
template<class input_type, class func_type>
void rfor_each(input_type&& input, func_type&& func) 
{ for(auto&& i : input) rfor_each(i, func);}

Однако, это (очевидно) дает нам ошибки двусмысленности. Поэтому мы используем SFINAE для определения того, соответствует ли текущий вход функции или нет

//Compiler knows to only use this if it can pass input to func
template<class input_type, class func_type>
auto rfor_each(input_type&& input, func_type&& func) ->decltype(func(input)) 
{ return func(input);}

//Otherwise, it always uses this one
template<class input_type, class func_type>
void rfor_each(input_type&& input, func_type&& func) 
{ for(auto&& i : input) rfor_each(i, func);}

Это теперь правильно обрабатывает контейнеры, но компилятор все еще считает это неоднозначным для input_types, который может быть передан функции. Поэтому мы используем стандартный трюк С++ 03, чтобы он предпочел первую функцию над второй, также пропустив нуль, и сделав тот, который мы предпочитаем accept и int, а другой принимает...

template<class input_type, class func_type>
auto rfor_each(input_type&& input, func_type&& func, int) ->decltype(func(input)) 
{ return func(input);}

//passing the zero causes it to look for a function that takes an int
//and only uses ... if it absolutely has to 
template<class input_type, class func_type>
void rfor_each(input_type&& input, func_type&& func, ...) 
{ for(auto&& i : input) rfor_each(i, func, 0);}

Что это. Шесть, относительно простых строк кода, и вы можете выполнять итерацию по значениям, строкам или любому другому подразделению, в отличие от всех других ответов.

#include <iostream>
int main()
 {

     std::cout << std::endl;
     double B[3][3] = { { 1.2 } };
     rfor_each(B[1], [](double&v){v = 5;}); //iterate over doubles
     auto write = [](double (&i)[3]) //iterate over rows
         {
             std::cout << "{";
             for(double d : i) 
                 std::cout << d << ", ";
             std::cout << "}\n";
         };
     rfor_each(B, write );
 };

Доказательство компиляции и исполнения здесь и здесь

Если вам нужен более удобный синтаксис в С++ 11, вы можете добавить макрос. (Следование не проверено)

template<class container>
struct container_unroller {
    container& c;
    container_unroller(container& c_) :c(c_) {}
    template<class lambda>
    void operator <=(lambda&& l) {rfor_each(c, l);}
};
#define FOR_NESTED(type, index, container) container_unroller(container) <= [](type& index) 
//note that this can't handle functions, function pointers, raw arrays, or other complex bits

int main() {
     double B[3][3] = { { 1.2 } };
     FOR_NESTED(double, v, B) {
         std::cout << v << ", ";
     }
}

Ответ 9

Я был шокирован тем, что никто не предлагал петлю на основе арифметической магии, чтобы выполнить эту работу. Так как C. Wang ищет решение без вложенных циклов, я предложу один из них:

double B[10][8][5];
int index = 0;

while (index < (10 * 8 * 5))
{
    const int x = index % 10,
              y = (index / 10) % 10,
              z = index / 100;

    do_something_on_B(B[x][y][z]);
    ++index;
}

Ну, этот подход не изящный и гибкий, поэтому мы могли бы упаковать весь процесс в функцию шаблона:

template <typename F, typename T, int X, int Y, int Z>
void iterate_all(T (&xyz)[X][Y][Z], F func)
{
    const int limit = X * Y * Z;
    int index = 0;

    while (index < limit)
    {
        const int x = index % X,
                  y = (index / X) % Y,
                  z = index / (X * Y);

        func(xyz[x][y][z]);
        ++index;
    }
}

Эта функция шаблона также может быть выражена в виде вложенных циклов:

template <typename F, typename T, int X, int Y, int Z>
void iterate_all(T (&xyz)[X][Y][Z], F func)
{
    for (auto &yz : xyz)
    {
        for (auto &z : yz)
        {
            for (auto &v : z)
            {
                func(v);
            }
        }
    }
}

И можно использовать, предоставляя 3D-массив произвольного размера плюс имя функции, позволяя вычитанию параметра выполнять сложную работу по подсчету размера каждого измерения:

int main()
{
    int A[10][8][5] = {{{0, 1}, {2, 3}}, {{4, 5}, {6, 7}}};
    int B[7][99][8] = {{{0, 1}, {2, 3}}, {{4, 5}, {6, 7}}};

    iterate_all(A, do_something_on_A);
    iterate_all(B, do_something_on_B);

    return 0;
}

На пути к более общим

Но опять-таки, ему не хватает гибкости, потому что он работает только для 3D-массивов, но используя SFINAE мы можем выполнять работу для массивов произвольного измерения, сначала нам нужна функция шаблона, которая выполняет итерации массивов rank 1:

template<typename F, typename A>
typename std::enable_if< std::rank<A>::value == 1 >::type
iterate_all(A &xyz, F func)
{
    for (auto &v : xyz)
    {
        func(v);
    }
}

И еще один, который выполняет итерации массивов любого ранга, делая рекурсию:

template<typename F, typename A>
typename std::enable_if< std::rank<A>::value != 1 >::type
iterate_all(A &xyz, F func)
{
    for (auto &v : xyz)
    {
        iterate_all(v, func);
    }
}

Это позволяет нам перебирать все элементы во всех размерах произвольно-размерного массива произвольного размера.


Работа с std::vector

Для многократного вложенного вектора решение ressembles одно из произвольно размерного массива произвольного размера, но без SFINAE: сначала нам понадобится функция шаблона, которая выполняет итерацию std::vector и вызывает нужную функцию:

template <typename F, typename T, template<typename, typename> class V>
void iterate_all(V<T, std::allocator<T>> &xyz, F func)
{
    for (auto &v : xyz)
    {
        func(v);
    }
}

И еще одна функция шаблона, которая выполняет итерацию любого вектора векторов и вызывает себя:

template <typename F, typename T, template<typename, typename> class V> 
void iterate_all(V<V<T, std::allocator<T>>, std::allocator<V<T, std::allocator<T>>>> &xyz, F func)
{
    for (auto &v : xyz)
    {
        iterate_all(v, func);
    }
}

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

int main()
{
    using V0 = std::vector< std::vector< std::vector<int> > >;
    using V1 = std::vector< std::vector< std::vector< std::vector< std::vector<int> > > > >;

    V0 A0 =   {{{0, 1}, {2, 3}}, {{4, 5}, {6, 7}}};
    V1 A1 = {{{{{9, 8}, {7, 6}}, {{5, 4}, {3, 2}}}}};

    iterate_all(A0, do_something_on_A);
    iterate_all(A1, do_something_on_A);

    return 0;
}

Я думаю, что тело функции довольно простое и прямолинейное... Интересно, может ли компилятор развернуть эти циклы (я почти уверен, что большинство компиляторов могут развернуть первый пример).

Смотрите живую демонстрацию здесь.

Надеюсь, что это поможет.

Ответ 10

Используйте что-то в этих строках (его псевдокод, но идея остается прежней). Вы извлекаете шаблон в цикл один раз и каждый раз применяете другую функцию.

doOn( structure A, operator o)
{
    for (int k=0; k<A.size(); k++)
    {
            for (int i=0; i<A[k].size(); i++)
            {
                for (int j=0; j<A[k][i].size(); j++)
                {
                        o.actOn(A[k][i][j]);
                }
            }
    }
}

doOn(a, function12)
doOn(a, function13)

Ответ 11

Придерживайтесь вложенных циклов!

Все предлагаемые здесь методы имеют недостатки с точки зрения удобочитаемости или гибкости.

Что произойдет, если вам нужно использовать результаты внутреннего цикла для обработки во внешнем цикле? Что произойдет, если вам понадобится значение из внешнего цикла внутри вашего внутреннего цикла? Большинство методов "инкапсуляции" здесь не работают.

Поверьте мне, я видел несколько попыток "очистить" вложенные для циклов, и в итоге выясняется, что вложенный цикл на самом деле является самым чистым и гибким решением.

Ответ 12

Один из методов, который я использовал, - это шаблоны. Например:.

template<typename T> void do_something_on_A(std::vector<T> &vec) {
    for (auto& i : vec) { // can use a simple for loop in C++03
        do_something_on_A(i);
    }
}

void do_something_on_A(int &val) {
    // this is where your `do_something_on_A` method goes
}

Затем вы просто вызываете do_something_on_A(A) в свой основной код. Функция шаблона создается один раз для каждого измерения, первый раз с T = std::vector<std::vector<int>>, второй раз с помощью T = std::vector<int>.

Вы можете сделать это более общим с помощью std::function (или функциональноподобных объектов в С++ 03) в качестве второго аргумента, если хотите:

template<typename T> void do_something_on_vec(std::vector<T> &vec, std::function &func) {
    for (auto& i : vec) { // can use a simple for loop in C++03
        do_something_on_vec(i, func);
    }
}

template<typename T> void do_something_on_vec(T &val, std::function &func) {
    func(val);
}

Затем назовите его так:

do_something_on_vec(A, std::function(do_something_on_A));

Это работает, хотя функции имеют одну и ту же подпись, потому что первая функция лучше подходит для чего-либо с std::vector в типе.

Ответ 13

Вот реализация С++ 11, которая обрабатывает все итерабельность. Другие решения ограничиваются контейнерами с ::iterator typedefs или массивами: но for_each - это итерация, а не контейнер.

Я также выделяю SFINAE на одно пятно в признаке is_iterable. Отправка (между элементами и итерами) выполняется посредством диспетчеризации тегов, которую я нахожу более ясным решением.

Контейнеры и функции, применяемые к элементам, полностью переадресованы, что позволяет как const, так и не const обращаться к диапазонам и функторам.

#include <utility>
#include <iterator>

Я использую функцию шаблона. Все остальное может войти в пространство имен деталей:

template<typename C, typename F>
void for_each_flat( C&& c, F&& f );

Отправка тегов намного чище, чем SFINAE. Эти два используются для итерируемых объектов и неистребимых объектов соответственно. Последняя итерация первого может использовать идеальную пересылку, но я ленив:

template<typename C, typename F>
void for_each_flat_helper( C&& c, F&& f, std::true_type /*is_iterable*/ ) {
  for( auto&& x : std::forward<C>(c) )
    for_each_flat(std::forward<decltype(x)>(x), f);
}
template<typename D, typename F>
void for_each_flat_helper( D&& data, F&& f, std::false_type /*is_iterable*/ ) {
  std::forward<F>(f)(std::forward<D>(data));
}

Это шаблон, необходимый для записи is_iterable. Я выполняю поиск зависимых от аргументов в begin и end в пространстве имен деталей. Это эмулирует то, что цикл for( auto x : y ) работает достаточно хорошо:

namespace adl_aux {
  using std::begin; using std::end;
  template<typename C> decltype( begin( std::declval<C>() ) ) adl_begin(C&&);
  template<typename C> decltype( end( std::declval<C>() ) ) adl_end(C&&);
}
using adl_aux::adl_begin;
using adl_aux::adl_end;

TypeSink полезно проверить, действительно ли код. Вы выполняете TypeSink< decltype( код ) >, и если значение code допустимо, выражение равно void. Если код недействителен, SFINAE запускается и специализация блокируется:

template<typename> struct type_sink {typedef void type;};
template<typename T> using TypeSink = typename type_sink<T>::type;

template<typename T, typename=void>
struct is_iterable:std::false_type{};
template<typename T>
struct is_iterable<T, TypeSink< decltype( adl_begin( std::declval<T>() ) ) >>:std::true_type{};

Я тестирую только begin. Также можно выполнить тест adl_end.

Окончательная реализация for_each_flat заканчивается чрезвычайно простой:

template<typename C, typename F>
void for_each_flat( C&& c, F&& f ) {
  for_each_flat_helper( std::forward<C>(c), std::forward<F>(f), is_iterable<C>() );
}        

Живой пример

Это снизу внизу: не стесняйтесь браться за верхние ответы, которые являются твердыми. Я просто хотел использовать несколько лучших методов!

Ответ 14

Вы можете генерировать индексы в одном цикле, подобном этому (A, B, C - размеры):

int A = 4, B = 3, C = 3;
for(int i=0; i<A*B*C; ++i)
{
    int a = i/(B*C);
    int b = (i-((B*C)*(i/(B*C))))/C;
    int c = i%C;
}

Ответ 15

Одна вещь, которую вы, возможно, захотите попробовать, если у вас есть только инструкции во внутреннем цикле - и ваша озабоченность связана с чрезмерно подробным характером кода - это использование другой схемы пробелов. Это будет работать только в том случае, если вы можете констатировать ваши циклы достаточно компактно, чтобы все они соответствовали одной строке.

В вашем первом примере я бы переписал его как:

vector< vector< vector<int> > > A;
int i,j,k;
for(k=0;k<A.size();k++) for(i=0;i<A[k].size();i++) for(j=0;j<A[k][i].size();j++) {
    do_something_on_A(A[k][i][j]);
}

Это отталкивает его, потому что вы вызываете функции во внешних циклах, что эквивалентно размещению в них утверждений. Я удалил все ненужное пустое пространство, и это может быть сносно.

Второй пример намного лучше:

double B[10][8][5];
int i,j,k;

for(k=0;k<10;k++) for(i=0;i<8;i++) for(j=0;j<5;j++) {
    do_something_on_B(B[k][i][j]);
}

Это может быть другое соглашение о пробелах, которое вы предпочитаете использовать, но оно обеспечивает компактный результат, который, тем не менее, не требует каких-либо знаний за пределами C/С++ (например, соглашений о макросах) и не требует каких-либо обманных макросов.

Если вам действительно нужен макрос, вы можете сделать следующий шаг:

#define FOR3(a,b,c,d,e,f,g,h,i) for(a;b;c) for(d;e;f) for(g;h;i)

который изменит второй пример на:

double B[10][8][5];
int i,j,k;

FOR3(k=0,k<10,k++,i=0,i<8,i++,j=0,j<5,j++) {
    do_something_on_B(B[k][i][j]);
}

и первый пример также лучше:

vector< vector< vector<int> > > A;
int i,j,k;
FOR3(k=0,k<A.size(),k++,i=0,i<A[k].size(),i++,j=0,j<A[k][i].size(),j++) {
    do_something_on_A(A[k][i][j]);
}

Надеюсь, вы можете довольно легко сказать, какие утверждения идут, для чего для операторов. Кроме того, остерегайтесь запятых, теперь вы не можете использовать их в одном разделе любого из for s.

Ответ 16

Во-первых, вы не должны использовать вектор векторов векторов. Гарантируется, что каждый вектор имеет непрерывную память, но "глобальная" память вектора векторов не является (и, вероятно, не будет). Вы должны использовать стандартный массив типов библиотек вместо массивов в стиле C.

using std::array;

array<array<array<double, 5>, 8>, 10> B;
for (int k=0; k<10; k++)
    for (int i=0; i<8; i++)
        for (int j=0; j<5; j++)
            do_something_on_B(B[k][i][j]);

// or, if you really don't like that, at least do this:

for (int k=0; k<10; k++) {
    for (int i=0; i<8; i++) {
        for (int j=0; j<5; j++) {
            do_something_on_B(B[k][i][j]);
        }
    }
}

Тем не менее, вы можете определить простой класс 3D-матрицы:

#include <stdexcept>
#include <array>

using std::size_t;

template <size_t M, size_t N, size_t P>
class matrix3d {
    static_assert(M > 0 && N > 0 && P > 0,
                  "Dimensions must be greater than 0.");
    std::array<std::array<std::array<double, P>, N>, M> contents;
public:
    double& at(size_t i, size_t j, size_t k)
    { 
        if (i >= M || j >= N || k >= P)
            throw out_of_range("Index out of range.");
        return contents[i][j][k];
    }
    double& operator(size_t i, size_t j, size_t k)
    {
        return contents[i][j][k];
    }
};

int main()
{
    matrix3d<10, 8, 5> B;
        for (int k=0; k<10; k++)
            for (int i=0; i<8; i++)
                for (int j=0; j<5; j++)
                    do_something_on_B(B(i,j,k));
    return 0;
}

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

Вы также можете добавить прокси-объекты, чтобы вы могли делать B [i] или B [i] [j]. Они могли бы возвращать векторы (в математическом смысле) и матрицы, полные double &, потенциально?