我有一个在内存中连续表示的多维数组。我希望隐藏这种表示,让用户访问数组元素就像它是多维的一样:例如my_array[0][3][5]
或my_array(0,3,5)
或类似内容。直到运行时才确定对象的尺寸,但是使用指定它具有多少维度的类型创建对象。这个元素查找需要被调用数十亿次,因此希望每次调用都需要最小的开销。
我看过类似的问题,但没有真正找到一个好的解决方案。使用[]
运算符需要创建N-1
维对象,这对于像向量矢量这样的多维结构很好,因为对象已经存在,但对于连续数组,它似乎会很快就会卷曲,并需要通过原始数组进行某种切片。
我还研究了重载()
,这似乎更有希望,但需要指定参数的数量,这将根据数组的维数而变化。我曾考虑使用列表初始化或向量,但希望避免实例化对象。
我对模板和图表有点熟悉,应该有一些方法可以使用C ++的宏伟模板功能来为不同类型的数组指定()
的唯一重载(例如,不同的尺寸数量)。但是我只在非常基本的通用案例中使用了模板,比如使用float
和double
来创建函数。
我想象的是这样的事情:
template<typename TDim>
class MultiArray {
public:
MultiArray() {} //build some things
~MultiArray() {} //destroy some things
// The number of arguments would be == to TDim for the instantiated class
float& operator() (int dim1, int dim2, ...) {
//convert to contiguous index and return ref to element
// I believe the conversion equation is something like:
// dim1 + Maxdim1 * ( dim2 + MaxDim2 * ( dim3 + MaxDim3 * (...)))
}
private:
vector<float> internal_array;
vector<int> MaxDimX; // Each element says how large each corresponding dim is.
};
因此,如果我初始化此类并尝试访问元素,它将如下所示:
my_array = MultiArray<4>();
element = my_array(2,5,4,1);
我如何使用模板进行此操作?这甚至可能吗?
答案 0 :(得分:6)
template<class T>
struct slice {
T* data = 0;
std::size_t const* stride = 0;
slice operator[](std::size_t I)const {
return{ data + I* *stride, stride + 1 };
}
operator T&()const {
return *data;
}
T& operator=(typename std::remove_const<T>::type in)const {
*data = std::move(in); return *data;
}
};
存储vector<T>
个数据和std::vector<std::size_t> stride
步幅,其中stride[0]
是第一个索引所需的元素步幅。
template<class T>
struct buffer {
std::vector<T> data;
std::vector<std::size_t> strides;
buffer( std::vector<std::size_t> sizes, std::vector<T> d ):
data(std::move(d)),
strides(sizes)
{
std::size_t scale = 1;
for (std::size_t i = 0; i<sizes.size(); ++i){
auto next = scale*strides[sizes.size()-1-i];
strides[sizes.size()-1-i] = scale;
scale=next;
}
}
slice<T> get(){ return {data.data(), strides.data()}; }
slice<T const> get()const{ return {data.data(), strides.data()}; }
};
如果你使用的[]
s不够,它会引用所讨论的子阵列的第一个元素。如果你使用太多,那就是UB。无论是尺寸还是尺寸,它都会进行零尺寸检查。
两者都可以添加,但会降低性能。
维度数量是动态的。您可以将buffer
拆分为两种类型,一种拥有缓冲区,另一种提供尺寸视图。
答案 1 :(得分:3)
在我看来,您可以使用Boost.MultiArray,boost::multi_array_ref
来更具体。 boost::multi_array_ref
完全符合您的要求:它将连续数据数组包装到一个可被视为多维数组的对象中。您也可以使用boost::multi_array_ref::array_view
进行切片。
我无法向您提供任何基准测试结果,但根据我的经验,我可以说boost::multi_array_ref
工作得非常快。
答案 2 :(得分:1)
如果你可以使用C ++ 17,那么可变参数模板折叠和row major order,我想你可以编写类似的东西(警告:未经测试)
template <template ... Args>
float & operator() (Args ... dims)
{
static_assert( sizeof...(Args) == TDim , "wrong number of indexes" );
// or SFINAE enable instead of static_assert()
std::size_t pos { 0U };
std::size_t i { 0U };
( pos *= MaxDimX[i++], pos += dims, ... );
return internal_array[pos];
}
OTPS(Off Topic Post Scriptum):如果我理解正确,你的MaxDimX
是一个维度向量;所以应该是无符号整数,非签名int
;通常,对于索引,使用std::size_t
[见注1]。
OTPS 2:如果你知道编译时的维度数(TDim
,对吗?)而不是std::vector
,我建议使用std::array
;我的意思是
std::array<std::size_t, TDim> MaxDimX;
- 编辑 -
如果你不能使用C ++ 17,你可以使用未使用的数组初始化技巧来获得类似的东西。
我的意思是
template <template ... Args>
float & operator() (Args ... dims)
{
using unused = int[];
static_assert( sizeof...(Args) == TDim , "wrong number of indexes" );
// or SFINAE enable instead of static_assert()
std::size_t pos { 0U };
std::size_t i { 0U };
(void)unused { (pos *= MaxDimX[i++], pos += dims, 0) ... };
return internal_array[pos];
}
注1:正如Julius指出的那样,对索引使用有符号或无符号整数是有争议的。
所以我试着更好地解释为什么我建议使用无符号值(例如std::size_t
)。
关键是(据我所知)所有标准模板库都设计为使用无符号整数作为索引值。您可以通过size()
方法返回的值以及接收索引的访问方法(at()
或operator[]
)接收无符号值来查看它。
对或错,语言本身旨在从旧std::size_t
和最近的可变参数sizeof()
返回sizeof...()
。同一个班级std::index_sequence
是std::integer_sequence
的别名,其中包含固定的无符号std::size_t
类型。
在一个设计为索引使用无符号整数的世界中,使用有符号整数可能,但恕我直言,危险(因为容易出错)。
答案 3 :(得分:1)
在创建具有可变尺寸的矩阵类的类模板时,我已多次使用此模式。
<强> Matrix.h 强>
#ifndef MATRIX_H
template<typename Type, size_t... Dims>
class Matrix {
public:
static const size_t numDims_ = sizeof...(Dims);
private:
size_t numElements_;
std::vector<Type> elements_;
std::vector<size_t> strides_; // Technically this vector contains the size of each dimension... (its shape)
// actual strides would be the width in memory of each element to that dimension of the container.
// A better name for this container would be dimensionSizes_ or shape_
public:
Matrix() noexcept;
template<typename... Arg>
Matrix( Arg&&... as ) noexcept;
const Type& operator[]( size_t idx ) const;
size_t numElements() const {
return elements_.size();
}
const std::vector<size_t>& strides() const {
return strides_;
}
const std::vector<Type>& elements() const {
return elements_;
}
}; // matrix
#include "Matrix.inl"
#endif // MATRIX_H
<强> Matrix.inl 强>
template<typename Type, size_t... Dims>
Matrix<Type, Dims...>::Matrix() noexcept :
strides_( { Dims... } ) {
using std::begin;
using std::end;
auto mult = std::accumulate( begin( strides_ ), end( strides_ ), 1, std::multiplies<>() );
numElements_ = mult;
elements_.resize( numElements_ );
} // Matrix
template<typename Type, size_t... Dims>
template<typename... Arg>
Matrix<Type, Dims...>::Matrix( Arg&&... as ) noexcept :
elements_( { as... } ),
strides_( { Dims... } ){
numElements_ = elements_.size();
} // Matrix
template<typename T, size_t... d>
const T& Matrix<T,d...>::operator[]( size_t idx ) const {
return elements_[idx];
} // Operator[]
<强> Matrix.cpp 强>
#include "Matrix.h"
#include <vector>
#include <numeric>
#include <functional>
#include <algorithm>
<强>的main.cpp 强>
#include <vector>
#include <iostream>
#include "matrix.h"
int main() {
Matrix<int, 3, 3> mat3x3( 1, 2, 3, 4, 5, 6, 7, 8, 9 );
for ( size_t idx = 0; idx < mat3x3.numElements(); idx++ ) {
std::cout << mat3x3.elements()[idx] << " ";
}
std::cout << "\n\nnow using array index operator\n\n";
for ( size_t idx = 0; idx < mat3x3.numElements(); idx++ ) {
std::cout << mat3x3[idx] << " ";
}
std::cout << "\n\ncheck the strides\n\n";
for ( size_t idx = 0; idx < mat3x3.numDims_; idx++ ) {
std::cout << mat3x3.strides()[idx] << " ";
}
std::cout << "\n\n";
std::cout << "=================================\n\n";
Matrix<float, 5, 2, 9, 7> mf5x2x9x7;
// Check Strides
// Num Elements
// Total Size
std::cout << "The total number of dimensions are: " << mf5x2x9x7.numDims_ << "\n";
std::cout << "The total number of elements are: " << mf5x2x9x7.numElements() << "\n";
std::cout << "These are the strides: \n";
for ( size_t n = 0; n < mf5x2x9x7.numDims_; n++ ) {
std::cout << mf5x2x9x7.strides()[n] << " ";
}
std::cout << "\n";
std::cout << "The elements are: ";
for ( size_t n = 0; n < mf5x2x9x7.numElements(); n++ ) {
std::cout << mf5x2x9x7[n] << " ";
}
std::cout << "\n";
std::cout << "\nPress any key and enter to quit." << std::endl;
char c;
std::cin >> c;
return 0;
} // main
这是Same Type <T>
您可以创建不同大小的浮点数,整数,字符等矩阵,例如2x2
,2x3
,5x3x7
,4x9x8x12x2x19
。这是一个非常简单但功能多样的课程。
正在使用std::vector<>
,因此搜索时间是线性的。多维矩阵尺寸越大,内部容器越大,每个尺寸的大小越大;如果每个单独的维度具有较大的维度大小,则可以相当快速地“爆炸”:例如9x9x9
只有3 dimensional volumetric matrix
,其元素多于2x2x2x2x2
,而5 dimensional volumetric matrix
是{ {1}}。第一个矩阵具有729
个元素,其中第二个矩阵只有32
个元素。
我没有包含默认构造函数,复制构造函数,移动构造函数,也没有包含std::container<T>
或其他Matrix<T,...>
的任何重载构造函数。这可以作为OP的练习来完成。
我也没有包含任何能够提供主容器中总元素大小的简单函数,也不包括 strides
容器大小的总维度数。 OP应该能够非常简单地实现这些。
对于strides
以及使用多维坐标进行索引,OP需要使用stride
值来再次计算适当的索引。我将此作为主要练习。
编辑 - 我继续添加了默认构造函数,将一些成员移动到了类的私有部分,并添加了一些访问函数。我这样做是因为我只想在main函数中展示这个类的强大功能,即使在创建一个类型的空容器时也是如此。
更多的是你可以用他的“步幅和切片”算法来获取用户Yakk的答案,并且应该能够轻松地将其插入到本课程中,为您提供所需内容的全部功能。