3

I am trying to make class that acts as multidimensional vector. It doesn't have to do anything fancy. I basically want to have a "container" class foo where I can access elements by foo[x][y][z]. Now I would also need similar classes for foo[x][y] and foo[x]. Which lead me to ponder about the following (more general) question, is there a way to make something like this where you can just initialize as foo A(a,b,c,...) for any n number of arguments and get a n-dimensional vector with elements accessible by [][][]...? Below the class I have for (in example) the four-dimensional case.

First the header

   #ifndef FCONTAINER_H
   #define FCONTAINER_H
   #include <iostream>
   using namespace std;

   class Fcontainer
   {
   private:
           unsigned dim1, dim2, dim3, dim4 ;
           double* data;
   public:
           Fcontainer(unsigned const dims1, unsigned const dims2, unsigned const dims3, unsigned const dims4);
           ~Fcontainer();

           Fcontainer(const Fcontainer& m);
           Fcontainer& operator= (const Fcontainer& m);

           double& operator() (unsigned const dim1, unsigned const dim2, unsigned const dim3, unsigned const dim4);
           double const& operator() (unsigned const dim1, unsigned const dim2, unsigned const dim3, unsigned const dim4) const;
    };
    #endif // FCONTAINER_H

Now the cpp:

  #include "fcontainer.hpp"

  Fcontainer::Fcontainer(unsigned const dims1, unsigned const dims2, unsigned const dims3, unsigned const dims4)
  {
       dim1 = dims1; dim2 = dims2; dim3 = dims3; dim4 = dims4;
       if (dims1 == 0 || dims2 == 0 || dims3 == 0 || dims4 == 0)
          throw std::invalid_argument("Container constructor has 0 size");
       data = new double[dims1 * dims2 * dims3 * dims4];
  }

  Fcontainer::~Fcontainer()
  {
      delete[] data;
  }

  double& Fcontainer::operator() (unsigned const dims1, unsigned const dims2, unsigned const dims3, unsigned const dims4)
  {
       if (dims1 >= dim1 || dims2 >= dim2 || dims3 >= dim3 || dims4 >= dim4)
           throw std::invalid_argument("Container subscript out of bounds");
       return data[dims1*dim2*dims3*dim4 + dims2*dim3*dim4 + dim3*dim4 + dims4];
  }

  double const& Fcontainer::operator() (unsigned const dims1, unsigned const dims2, unsigned const dims3, unsigned const dims4) const
  {
     if(dims1 >= dim1 || dims2 >= dim2 || dims3 >= dim3 || dims4 >= dim4)
         throw std::invalid_argument("Container subscript out of bounds");
      return data[dims1*dim2*dims3*dim4 + dims2*dim3*dim4 + dim3*dim4 + dims4];
  }

So I want to expand this to an arbitrary amount of dimensions. I suppose it will take something along the lines of a variadic template or an std::initializer_list but I am not clear on how to approach this( for this problem).

  • 1
    Would you be okay with `my_container(x,y,z, ... ,n)` instead of `my_container[x][y][z]...[n]`? – NathanOliver Aug 17 '17 at 19:15
  • What exactly are you unsure of? How to initialize dimensions for a matrix of n dimensions? Or something else? – SergeyA Aug 17 '17 at 19:20
  • Here is a related post where I suggest such a method: https://stackoverflow.com/questions/43358369/c-n-nested-vectors-at-runtime – NathanOliver Aug 17 '17 at 19:20
  • @NathanOliver, why? `[]` is easily implementable – SergeyA Aug 17 '17 at 19:20
  • [c++11 variadic programming, how to define a tower of vectors](https://stackoverflow.com/questions/24463356/c11-variadic-programming-how-to-define-a-tower-of-vectors) might be something that could fit your needs. – R Sahu Aug 17 '17 at 19:26
  • 1
    @SergeyA On the topic of [] vs (), this might interest you: https://isocpp.org/wiki/faq/operator-overloading#matrix-c-style-subscript – Jonas Verhellen Aug 17 '17 at 19:55
  • @SergeyA The thing I am unsure of (for starters) is how to initialize the above class for a generic dimensionality. – Jonas Verhellen Aug 17 '17 at 19:57

2 Answers2

2

Messing around in Visual Studio for a little while, I came up with this nonsense:

template<typename T>
class Matrix {
    std::vector<size_t> dimensions;
    std::unique_ptr<T[]> _data;

    template<typename ... Dimensions>
    size_t apply_dimensions(size_t dim, Dimensions&& ... dims) {
        dimensions.emplace_back(dim);
        return dim * apply_dimensions(std::forward<Dimensions>(dims)...);
    }

    size_t apply_dimensions(size_t dim) {
        dimensions.emplace_back(dim);
        return dim;
    }
public:
    Matrix(std::vector<size_t> dims) : dimensions(std::move(dims)) {
        size_t size = flat_size();
        _data = std::make_unique<T[]>(size);
    }

    template<typename ... Dimensions>
    Matrix(size_t dim, Dimensions&&... dims) {
        size_t size = apply_dimensions(dim, std::forward<Dimensions>(dims)...);
        _data = std::make_unique<T[]>(size);
    }

    T & operator()(std::vector<size_t> const& indexes) {
        if(indexes.size() != dimensions.size())
            throw std::runtime_error("Incorrect number of parameters used to retrieve Matrix Data!");
        return _data[get_flat_index(indexes)];
    }

    T const& operator()(std::vector<size_t> const& indexes) const {
        if (indexes.size() != dimensions.size())
            throw std::runtime_error("Incorrect number of parameters used to retrieve Matrix Data!");
        return _data[get_flat_index(indexes)];
    }

    template<typename ... Indexes>
    T & operator()(size_t idx, Indexes&& ... indexes) {
        if (sizeof...(indexes)+1 != dimensions.size())
            throw std::runtime_error("Incorrect number of parameters used to retrieve Matrix Data!");
        size_t flat_index = get_flat_index(0, idx, std::forward<Indexes>(indexes)...);
        return at(flat_index);
    }

    template<typename ... Indexes>
    T const& operator()(size_t idx, Indexes&& ... indexes) const {
        if (sizeof...(indexes)+1 != dimensions.size())
            throw std::runtime_error("Incorrect number of parameters used to retrieve Matrix Data!");
        size_t flat_index = get_flat_index(0, idx, std::forward<Indexes>(indexes)...);
        return at(flat_index);
    }

    T & at(size_t flat_index) {
        return _data[flat_index];
    }

    T const& at(size_t flat_index) const {
        return _data[flat_index];
    }

    size_t dimension_size(size_t dim) const {
        return dimensions[dim];
    }

    size_t num_of_dimensions() const {
        return dimensions.size();
    }

    size_t flat_size() const {
        size_t size = 1;
        for (size_t dim : dimensions)
            size *= dim;
        return size;
    }

private:
    size_t get_flat_index(std::vector<size_t> const& indexes) const {
        size_t dim = 0;
        size_t flat_index = 0;
        for (size_t index : indexes) {
            flat_index += get_offset(index, dim++);
        }
        return flat_index;
    }

    template<typename ... Indexes>
    size_t get_flat_index(size_t dim, size_t index, Indexes&& ... indexes) const {
        return get_offset(index, dim) + get_flat_index(dim + 1, std::forward<Indexes>(indexes)...);
    }

    size_t get_flat_index(size_t dim, size_t index) const {
        return get_offset(index, dim);
    }

    size_t get_offset(size_t index, size_t dim) const {
        if (index >= dimensions[dim])
            throw std::runtime_error("Index out of Bounds");
        for (size_t i = dim + 1; i < dimensions.size(); i++) {
            index *= dimensions[i];
        }
        return index;
    }
};

Let's talk about what this code accomplishes.

//private:
    template<typename ... Dimensions>
    size_t apply_dimensions(size_t dim, Dimensions&& ... dims) {
        dimensions.emplace_back(dim);
        return dim * apply_dimensions(std::forward<Dimensions>(dims)...);
    }

    size_t apply_dimensions(size_t dim) {
        dimensions.emplace_back(dim);
        return dim;
    }
public:
    Matrix(std::vector<size_t> dims) : dimensions(std::move(dims)) {
        size_t size = flat_size();
        _data = std::make_unique<T[]>(size);
    }

    template<typename ... Dimensions>
    Matrix(size_t dim, Dimensions&&... dims) {
        size_t size = apply_dimensions(dim, std::forward<Dimensions>(dims)...);
        _data = std::make_unique<T[]>(size);
    }

What this code enables us to do is write an initializer for this matrix that takes an arbitrary number of dimensions.

int main() {
    Matrix<int> mat{2, 2}; //Yields a 2x2 2D Rectangular Matrix
    mat = Matrix<int>{4, 6, 5};//mat is now a 4x6x5 3D Rectangular Matrix
    mat = Matrix<int>{9};//mat is now a 9-length 1D array.
    mat = Matrix<int>{2, 3, 4, 5, 6, 7, 8, 9};//Why would you do this? (yet it compiles...)
}

And if the number and sizes of the dimensions is only known at runtime, this code will work around that:

int main() {
    std::cout << "Input the sizes of each of the dimensions.\n";
    std::string line;
    std::getline(std::cin, line);
    std::stringstream ss(line);
    size_t dim;
    std::vector<size_t> dimensions;
    while(ss >> dim)
        dimensions.emplace_back(dim);

    Matrix<int> mat{dimensions};//Voila.
}

Then, we want to be able to access arbitrary indexes of this matrix. This code offers two ways to do so: either statically using templates, or variably at runtime.

//public:
    T & operator()(std::vector<size_t> const& indexes) {
        if(indexes.size() != dimensions.size())
            throw std::runtime_error("Incorrect number of parameters used to retrieve Matrix Data!");
        return _data[get_flat_index(indexes)];
    }

    T const& operator()(std::vector<size_t> const& indexes) const {
        if (indexes.size() != dimensions.size())
            throw std::runtime_error("Incorrect number of parameters used to retrieve Matrix Data!");
        return _data[get_flat_index(indexes)];
    }

    template<typename ... Indexes>
    T & operator()(size_t idx, Indexes&& ... indexes) {
        if (sizeof...(indexes)+1 != dimensions.size())
            throw std::runtime_error("Incorrect number of parameters used to retrieve Matrix Data!");
        size_t flat_index = get_flat_index(0, idx, std::forward<Indexes>(indexes)...);
        return at(flat_index);
    }

    template<typename ... Indexes>
    T const& operator()(size_t idx, Indexes&& ... indexes) const {
        if (sizeof...(indexes)+1 != dimensions.size())
            throw std::runtime_error("Incorrect number of parameters used to retrieve Matrix Data!");
        size_t flat_index = get_flat_index(0, idx, std::forward<Indexes>(indexes)...);
        return at(flat_index);
    }

And then, in practice:

Matrix<int> mat{6, 5};
mat(5, 2) = 17;
//mat(5, 1, 7) = 24; //throws exception at runtime because of wrong number of dimensions.
mat = Matrix<int>{9, 2, 8};
mat(5, 1, 7) = 24;
//mat(5, 2) = 17; //throws exception at runtime because of wrong number of dimensions.

And this works fine with runtime-dynamic indexing:

std::vector<size_t> indexes;
/*...*/
mat(indexes) = 54; //Will throw if index count is wrong, will succeed otherwise

There are a number of other functions that this kind of object might want, like a resize method, but choosing how to implement that is a high-level design decision. I've also left out tons of other potentially valuable implementation details (like an optimizing move-constructor, a comparison operator, a copy constructor) but this should give you a pretty good idea of how to start.

EDIT:

If you want to avoid use of templates entirely, you can cut like half of the code provided here, and just use the methods/constructor that uses std::vector<size_t> to provide dimensions/index data. If you don't need the ability to dynamically adapt at runtime to the number of dimensions, you can remove the std::vector<size_t> overloads, and possibly even make the number of dimensions a template argument for the class itself (which would enable you to use size_t[] or std::array[size_t, N] to store dimensional data).

Community
  • 1
  • 1
Xirema
  • 19,889
  • 4
  • 32
  • 68
  • Thank you. I quite like your solution. For my uses I do not need to dynamically adapt the number of dimensions at runtime but I was interested in the more general case anyway. Thanks again. – Jonas Verhellen Aug 18 '17 at 12:36
0

Well, assuming you care about efficiency at all, you probably want to store all of the elements in a contiguous manner regardless. So you probably want to do something like:

template <std::size_t N, class T>
class MultiArray {
    MultiArray(const std::array<std::size_t, N> sizes)
      : m_sizes(sizes)
      , m_data.resize(product(m_sizes)) {}

    std::array<std::size_t, N> m_sizes;
    std::vector<T> m_data;
};

The indexing part is where it gets kind of fun. Basically, if you want a[1][2][3] etc to work, you have to have a return some kind of proxy object, that has its own operator[]. Each one would have to be aware of its own rank. Each time you do [] it returns a proxy letting you specify the next index.

template <std::size_t N, class T>
class MultiArray {
   // as before

   template <std::size_t rank>
   class Indexor {
       Indexor(MultiArray& parent, const std::array<std::size_t, N>& indices = {})
         : m_parent(parent), m_indices(indices) {}

       auto operator[](std::size_t index) {
           m_indices[rank] = index;
           return Indexor<rank+1>(m_indices, m_parent);
       }
       std::array<std::size_t, N> m_indices;
       MultiArray& m_parent;
   };

   auto operator[](std::size_t index) {
       return Indexor<0>(*this)[index];
   }
}

Finally, you have a specialization for when you're done with the last index:

   template <>
   class Indexor<N-1> { // with obvious constructor
       auto operator[](std::size_t index) {
           m_indices[N-1] = index;
           return m_parent.m_data[indexed_product(m_indices, m_parent.m_sizes)];
       }
       std::array<std::size_t, N> m_indices;
       MultiArray& m_parent;
   };

Obviously this is a sketch but at this point its just filling out details and getting it to compile. There are other approaches like instead having the indexor object have two iterators and narrowing but that seemed a bit more complex. You also don't need to template the Indexor class and could use a runtime integer instead but that would make it very easy to misuse, having one too many or too few [] would be a runtime error, not compile time.

Edit: you would also be able to initialize this in the way you describe in 17, but not in 14. But in 14 you can just use a function:

template <class ... Ts>
auto make_double_array(Ts ts) {
    return MultiArray<sizeof ... Ts, double>(ts...);
}

Edit2: I use product and indexed_product in the implementation. The first is obvious, the second is less so, but hopefully they should be clear. The latter is a function that given an array of dimensions, and an array of indices, would return the position of that element in the array.

Nir Friedman
  • 17,108
  • 2
  • 44
  • 72