这可能是您的问题的简化版本,但是您使用的数据结构(“三星”数组)几乎从来都不是您想要的。如果您要创建像这里这样的密集矩阵,并分配每个元素的空间,进行数百万次微小分配根本没有任何优势。如果你想要一个稀疏矩阵,你通常需要一种像压缩稀疏行这样的格式。
如果数组是“矩形的”(或者我认为 3-D 的数组是“方形的”),并且所有行和列的大小相同,那么与分配单个内存块相比,这种数据结构纯粹是浪费.您执行了数百万次微小的分配,为数百万个指针分配空间,并丢失了内存的局部性。
此样板为动态 3-D 数组创建了零成本抽象。 (好吧,几乎:存储底层一维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,因为这使人们认为“二维”数组是指向行的指针的“参差不齐”数组。
在现实世界中,这几乎不是最适合这项工作的数据结构。