4

The rationale behind said array is to emulate a 2D/3D pixel matrix.

After doing a fair amount of research and reading, I reckon Boost.MultiArray might come in handy for this. However, I still have to create a neat wrapper on top of it to allow for less verbose coding.

Ultimately, what I want to achieve is the following:

PixMat<u8, 3, {2, 4, 3}> pixMat;

or

PixMat<u8, 3> pixMat(2,3,4);

, which would basically create a 2x4x3 matrix of u8 values.

What I've come up with so far is:

template <typename T, int Dims>
class PixMat {
public:
    typedef typename boost::multi_array<T, Dims> PixMatType;
    typedef typename PixMatType::index PixMatTypeIdx;
    PixMat(int dim1Ext, int dim2Ext) : pixMat(PixMatType(boost::extents[dim1Ext][dim2Ext])) {}
    PixMat(int dim1Ext, int dim2Ext, int dim3Ext) : pixMat(PixMatType(boost::extents[dim1Ext][dim2Ext][dim3Ext])) {}

private:
    PixMatType pixMat;
};

template <typename T>
class Pix2DMat : PixMat<T, 2> {
public:
    Pix2DMat(int dim1Ext, int dim2Ext) : PixMat<DataType, 2>(dim1Ext, dim2Ext) {}
};

template <typename T>
class Pix3DMat : PixMat<T, 3> {
public:
    Pix3DMat(int dim1Ext, int dim2Ext, int dim3Ext) : PixMat<DataType, 3>(dim1Ext, dim2Ext, dim3Ext) {}
};

I'm not too keen on this solution. Normally, the matrix won't be either 2D or 3D, but still, I'd like a more generic solution.

Questions:

  1. Is there a way to provide the extents of the dimensions as template arguments as well instead of via C-TOR?
  2. Is there a better way than inheritance to achieve this e.g. template specialization, variadic templates? But then how to deal with not duplicating the typedefs for boost all over the place?
razvanimal
  • 87
  • 5

2 Answers2

2

Here are a few techniques that I think you could use:

Variadic constructor argument

Rather than having a separate constructor for each possible dimension, you could use variadic argument techniques to create a generic N-dimensional constructor. Something that is your friend here: boost::extents is not required for the constructor argument, but instead anything that meets the requirements of a Collection. One example is just a plain STL or boost array:

template <typename... DimSizes>
PixMat(DimSizes&&... sizes)
 : pixMat(boost::array<std::size_t, sizeof...(DimSizes)>{{ static_cast<size_t>(sizes)...}}) {
}

This isn't the most polished implementation; in particular it doesn't place much of a requirement on DimSizes, which should really all be the same unsigned integer type (see this question for possible improvements). Also (for simplicity) perfect forwarding isn't implemented, but that probably just requires wrapping sizes with std::forward<DimSizes>(sizes) in the constructor. You can consult this stackoverflow post for possible alternative implementations.

Static assertion / SFINAE

Your template base class has a 2D constructor and 3D constructor --- or if you follow the above, a template N-dimensional constructor --- regardless of the value of the actual template parameter. You could use static assertion or SFINAE so that only the the Dims-D dimensional constructor is compilable. This will convert run-time bugs into compilation errors:

template <typename... DimSizes>
PixMat(DimSizes&&... sizes)
 : pixMat(boost::array<std::size_t, sizeof...(DimSizes)>{{ static_cast<size_t>(sizes)...}}) {
    static_assert(sizeof...(DimSizes) == Dims);
}

Dimension sizes as templates

I think this is a possible though completely orthogonal solution. It's implementation would follow from the above without too much work, but to make this interoperable with the constructor argument based solution would require a lot of hard work (i.e. to make them part of the same class or class hierarchy).

Other libraries

You might want to take a look at Eigen, for example. It does a lot of the aforementioned hard work.

jwimberley
  • 1,696
  • 11
  • 24
  • Although I've decided to adopt a completely different approach due to usage reasons, this answers the original question. Short and precise, good stuff! – razvanimal Nov 29 '18 at 15:10
2

If I understood you correctly, you want compile-time dimension, but run-time extents.

I would use a design like this:

template <typename T,std::size_t Dim>
class mdvector
{
private:
  std::vector<T> Data;
  std::array<std::size_t,Dim> Extents;

private:
  std::size_t Offset(std::size_t const Sum) const
    { return Sum; }

  template <typename... IndexType>
  std::size_t Offset(std::size_t const Sum,std::size_t const Index,IndexType const... Indices) const
    { return Offset(Sum*Extents[Dim-sizeof...(Indices)-1u]+Index,Indices...); }

public:
  template <typename... IndexType,typename= std::enable_if_t<sizeof...(IndexType)==Dim>>
  mdvector(IndexType const... Indices):
    Data((... * Indices)), // c++17 fold expression
    Extents{Indices...} {}

  template <typename... IndexType,typename= std::enable_if_t<sizeof...(IndexType)==Dim>>
  T const &operator()(IndexType const... Indices) const
    { return Data[Offset(0u,Indices...)]; }

  template <typename... IndexType,typename= std::enable_if_t<sizeof...(IndexType)==Dim>>
  T &operator()(IndexType const... Indices)
    { return Data[Offset(0u,Indices...)]; }
};

The data are stored in a std::vector while the extents make up an std::array.

Since I assume you want multi-dimensional access I have used a given mapping via the recursive function Offset. You have quite freedom in this regard.

Here's an example of use:

int main()
{
mdvector<double,3u> myvec(2u,3u,4u);

for (int i= 0; i<2; ++i)
  for (int j= 0; j<3; ++j)
    for (int k= 0; k<4; ++k)
      myvec(i,j,k)= i;

for (int k= 0; k<4; ++k)
  {
  for (int i= 0; i<2; ++i)
    {
    for (int j= 0; j<3; ++j)
      std::cout << myvec(i,j,k) << "\t";
    std::cout << "\n";
    }
  std::cout << "\n";
  }
}

Since the extents are dynamic, you can change them at runtime, for example:

template <typename T,std::size_t Dim>
template <typename... IndexType,typename= std::enable_if_t<sizeof...(IndexType)==Dim>>
void mdvector<T,Dim>::Resize(IndexType const... Indices)
  { Data.resize((... * Indices)); Extents= {Indices...}; }

If, on the other hand, you prefer the extents to be template parameters, they should stay fixed at runtime. In that case, you would include them in the class declaration:

template <typename T,std::size_t... Indices>
class mdvector { /* ... */};

but the implementation would be pretty much the same. Note that specifying the dimension would be unnecessary since you can get it with sizeof...(Indices).


The fold expression in the above code is not estrictly necessary. An equivalent outcome is achieved with:

static constexpr std::size_t Product()
  { return 1u; }

template <typename... IndexType>
static constexpr std::size_t Product(std::size_t const Index,IndexType const... Indices)
  { return Index*Product(Indices...); }

template <typename... IndexType,typename= std::enable_if_t<sizeof...(IndexType)==Dim>>
mdvector(IndexType const... Indices):
  Data(Product(Indices...)), Extents{Indices...} {}
metalfox
  • 6,301
  • 1
  • 21
  • 43