Вероятно, это была упрощенная версия вашей проблемы, но используемая вами структура данных («трехзвездочные» массивы) почти никогда не будет той, которую вы хотите. Если вы создаете плотную матрицу, как здесь, и выделяете места для каждого элемента, нет никакого преимущества в том, чтобы делать миллионы крошечных выделений. Если вы хотите разреженную матрицу, вам обычно нужен формат, подобный сжатой разреженной строке.
Если массив является «прямоугольным» (или я предполагаю, что трехмерный будет «квадратным»), и все строки и столбцы имеют одинаковый размер, эта структура данных является просто расточительной по сравнению с выделением одного блока памяти , Вы выполняете миллионы крошечных выделений, выделяете пространство для миллионов указателей и теряете локальность памяти.
Этот шаблон создает абстракцию с нулевой стоимостью для динамического трехмерного массива. (Хорошо, почти: избыточно хранить как длину базового одномерного std::vector
, так и отдельных измерений.) API использует a(i, j, k)
в качестве эквивалента a[i][j][k]
и a.at(i,j,k)
в качестве варианта с границами. проверка.
Этот API также имеет опцию для заполнения массива функцией индексов, f(i,j,k)
. Если вы звоните a.generate(f)
, он устанавливает каждый a(i,j,k) = f(i,j,k)
. Теоретически, эта сила уменьшает расчет смещения во внутреннем цикле, чтобы сделать его намного быстрее. API также может передавать генерирующую функцию в конструктор как array3d<float>(M, N, P, f)
. Расширьте его, как вам угодно.
#include <cassert>
#include <cstddef>
#include <cstdlib>
#include <functional>
#include <iomanip>
#include <iostream>
#include <vector>
using std::cout;
using std::endl;
using std::ptrdiff_t;
using std::size_t;
/* In a real-world implementation, this class would be split into a
* header file and a definitions file.
*/
template <typename T>
class array3d {
public:
using value_type = T;
using size_type = size_t;
using difference_type = ptrdiff_t;
using reference = T&;
using const_reference = const T&;
using pointer = T*;
using const_pointer = const T*;
using iterator = typename std::vector<T>::iterator;
using const_iterator = typename std::vector<T>::const_iterator;
using reverse_iterator = typename std::vector<T>::reverse_iterator;
using const_reverse_iterator = typename
std::vector<T>::const_reverse_iterator;
/* For this trivial example, I don’t define a default constructor or an API
* to resize a 3D array.
*/
array3d( const ptrdiff_t rows,
const ptrdiff_t cols,
const ptrdiff_t layers )
{
const ptrdiff_t nelements = rows*cols*layers;
assert(rows > 0);
assert(cols > 0);
assert(layers > 0);
assert(nelements > 0);
nrows = rows;
ncols = cols;
nlayers = layers;
storage.resize(static_cast<size_t>(nelements));
}
/* Variant that initializes an array with bounds and then fills each element
* (i,j,k) with a provided function f(i,j,k).
*/
array3d( const ptrdiff_t rows,
const ptrdiff_t cols,
const ptrdiff_t layers,
const std::function<T(ptrdiff_t, ptrdiff_t, ptrdiff_t)> f )
{
const ptrdiff_t nelements = rows*cols*layers;
assert(rows > 0);
assert(cols > 0);
assert(layers > 0);
assert(nelements > 0);
nrows = rows;
ncols = cols;
nlayers = layers;
storage.reserve(static_cast<size_t>(nelements));
for ( ptrdiff_t i = 0; i < nrows; ++i )
for ( ptrdiff_t j = 0; j < ncols; ++j )
for ( ptrdiff_t k = 0; k < nlayers; ++k )
storage.emplace_back(f(i,j,k));
assert( storage.size() == static_cast<size_t>(nelements) );
}
// Rule of 5:
array3d( const array3d& ) = default;
array3d& operator= ( const array3d& ) = default;
array3d( array3d&& ) = default;
array3d& operator= (array3d&&) = default;
/* a(i,j,k) is the equivalent of a[i][j][k], except that the indices are
* signed rather than unsigned. WARNING: It does not check bounds!
*/
T& operator() ( const ptrdiff_t i,
const ptrdiff_t j,
const ptrdiff_t k ) noexcept
{
return storage[make_index(i,j,k)];
}
const T& operator() ( const ptrdiff_t i,
const ptrdiff_t j,
const ptrdiff_t k ) const noexcept
{
return const_cast<array3d&>(*this)(i,j,k);
}
/* a.at(i,j,k) checks bounds. Error-checking is by assertion, rather than
* by exception, and the indices are signed.
*/
T& at( const ptrdiff_t i, const ptrdiff_t j, const ptrdiff_t k )
{
bounds_check(i,j,k);
return (*this)(i,j,k);
}
const T& at( const ptrdiff_t i,
const ptrdiff_t j,
const ptrdiff_t k ) const
{
return const_cast<array3d&>(*this).at(i,j,k);
}
/* Given a function or function object f(i,j,k), fills each element of the
* container with a(i,j,k) = f(i,j,k).
*/
void generate( const std::function<T(ptrdiff_t,
ptrdiff_t,
ptrdiff_t)> f )
{
iterator it = storage.begin();
for ( ptrdiff_t i = 0; i < nrows; ++i )
for ( ptrdiff_t j = 0; j < ncols; ++j )
for ( ptrdiff_t k = 0; k < nlayers; ++k )
*it++ = f(i,j,k);
assert(it == storage.end());
}
/* Could define a larger API, e.g. begin(), end(), rbegin() and rend() from the STL.
* Whatever you need.
*/
private:
ptrdiff_t nrows, ncols, nlayers;
std::vector<T> storage;
constexpr size_t make_index( const ptrdiff_t i,
const ptrdiff_t j,
const ptrdiff_t k ) const noexcept
{
return static_cast<size_t>((i*ncols + j)*nlayers + k);
}
// This could instead throw std::out_of_range, like STL containers.
constexpr void bounds_check( const ptrdiff_t i,
const ptrdiff_t j,
const ptrdiff_t k ) const
{
assert( i >=0 && i < nrows );
assert( j >= 0 && j < ncols );
assert( k >= 0 && k < nlayers );
}
};
// In a real-world scenario, this test driver would be in another source file:
constexpr float f( const ptrdiff_t i, const ptrdiff_t j, const ptrdiff_t k )
{
return static_cast<float>( k==0 ? 1.0 : -1.0 *
((double)i + (double)j*1E-4));
}
int main(void)
{
constexpr ptrdiff_t N = 2200, M = 4410, P = 2;
const array3d<float> a(N, M, P, f);
// Should be: -1234.4321
cout << std::setprecision(8) << a.at(1234,4321,1) << endl;
return EXIT_SUCCESS;
}
Стоит отметить, что этот код технически содержит неопределенное поведение: предполагается, что мультипликативное переполнение со знаком со знаком выдает отрицательное число, но на самом деле компилятор имеет право генерировать полностью неработающий код, если программа запрашивает некоторый абсурдный объем памяти во время выполнения. .
Конечно, если границы массива являются константами, просто объявите их constexpr
и используйте массив с фиксированными границами.
К сожалению, каждый новый программист C ++ сначала узнает о char** argv
, потому что это заставляет людей думать, что «двумерный» массив - это «рваный» массив указателей на строки.
В реальном мире это почти никогда не лучшая структура данных для работы.