HiveBrain v1.2.0
Get Started
← Back to all entries
patterncppMinor

Handy tool that provides easy syntax for specifying multi-dimensional types in C++

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
specifyingmultihandyprovidessyntaxeasythatfortypesdimensional

Problem

The syntax for specifying multi-dimensional types in C++ is messy. For example, to specify a two-dimensional std::vector, one has to write std::vector. The syntax doesn't scale. So, I wrote a handy tool that provides easy syntax for this. Any kinds of comments are welcome ^_^ (original version of the code can be found here with unit test code here)

template  class T, typename Base, std::size_t n>
struct multi {
  static_assert(n > 0, "");
  using type = T::type>;
};

template  class T, typename Base>
struct multi {
  using type = T;
};

/// Provides easy syntax for specifying multi-dimensional types.
/// For example, `multi_t` is equivalent to
/// `std::vector>`.
template  class T, typename Base, std::size_t n>
using multi_t = typename multi::type;

Solution

There's so little code here (and none of it anything that actually executes, just typedefs) that it's difficult to review the code as a thing in itself.

That leaves only the question of whether the basic idea of the code is a good one. At least in my opinion, the answer to that is a (heavily qualified) "no", at least as a general rule.

If you really need the equivalent of a muti-dimensional array, nested containers are rarely the best way to achieve that. Instead, you're usually better off with a single container and multi-dimensional indexing into that single container.

There are a couple of different ways of providing that. One is to overload operator() for the correct number of parameters (and optionally provide overloads for fewer parameters, which return entire collections rather than a single element). Another is to use multiple overloads of operator[], so indexing looks like a multi-dimensional array in C (e.g., x[i][j][k]), but the higher-order operators return proxy objects, so [i][j][k] ends up doing the multiplication necessary to find the correct element in the collection.

Relatively straightforward code for that (supporting only 3 dimensions) could look something like this:

#include 
#include 

template
class matrix3 {
    size_t a, b, c;
    std::vector data;

    friend class proxy;
    friend class proxy2;

    class proxy {
        matrix3 &m_;
        int index1_, index2_;
    public:
        proxy(matrix3 &m, int i1, int i2)
            : m_(m), index1_(i1), index2_(i2) { }

        T &operator[](int index3) {
            return m_.data[index1_ * m_.b * m_.c + index2_ * m_.c + index3];
        }
    };

    class proxy2 {
        matrix3 &m_;
        int index_;
    public:
        proxy2(matrix3 &m, int d) : m_(m), index_(d) { }

        proxy operator[](int index2) {
            return proxy(m_, index_, index2);
        }
    };
public:
    matrix3(size_t a, size_t b, size_t c) : a(a), b(b), c(c), data(a * b * c) { }

    proxy2 operator[](int index) {
        return proxy2(*this, index);
    }
};


Simple demo code:

int main() {
    matrix3 m(3, 3, 2);

    for (int x = 0; x < 3; x++)
        for (int y = 0; y < 3; y++)
            for (int z = 0; z < 2; z++)
                m[x][y][z] = x * 100 + y * 10 + z;

    for (int x = 0; x < 3; x++) {
        for (int y = 0; y < 3; y++) {
            for (int z = 0; z < 2; z++)
                std::cout << m[x][y][z] << "\t";
            std::cout << "\n";
        }
        std::cout << "\n\n";
    }    
}


I haven't tried to work through really doing it, but I believe you should be able to shorten (and possibly even simplify) that by defining the proxy classes using a non-type parameter for the stored index, and recursion to a proxy, where n is the dimension. Likewise, you'd need to define the operator[] to multiply and accumulate across N dimensions instead of the fixed number employed here.

This gives a number of advantages. First, improved locality of reference--all the data is allocated and accessed as a single, contiguous block. Second, reduce memory access--instead of a pointer to a pointer to a pointer to a block of data, you have only a single level of pointer to access, regardless of the number of dimensions. You do have to use some multiplications to avoid those memory accesses, but on a modern CPU that's a really good trade off.

Hope I haven't gotten too far afield from the original topic--this is quite a bit different code (really code for the entirety of a multi-dimensional array, not just typedefs for declaring/defining one), but it seems pretty obvious that if you're doing to declare/define one, you also want to use it...

Code Snippets

#include <vector>
#include <iostream>

template<class T>
class matrix3 {
    size_t a, b, c;
    std::vector<T> data;

    friend class proxy;
    friend class proxy2;

    class proxy {
        matrix3 &m_;
        int index1_, index2_;
    public:
        proxy(matrix3 &m, int i1, int i2)
            : m_(m), index1_(i1), index2_(i2) { }

        T &operator[](int index3) {
            return m_.data[index1_ * m_.b * m_.c + index2_ * m_.c + index3];
        }
    };

    class proxy2 {
        matrix3 &m_;
        int index_;
    public:
        proxy2(matrix3 &m, int d) : m_(m), index_(d) { }

        proxy operator[](int index2) {
            return proxy(m_, index_, index2);
        }
    };
public:
    matrix3(size_t a, size_t b, size_t c) : a(a), b(b), c(c), data(a * b * c) { }

    proxy2 operator[](int index) {
        return proxy2(*this, index);
    }
};
int main() {
    matrix3<double> m(3, 3, 2);

    for (int x = 0; x < 3; x++)
        for (int y = 0; y < 3; y++)
            for (int z = 0; z < 2; z++)
                m[x][y][z] = x * 100 + y * 10 + z;

    for (int x = 0; x < 3; x++) {
        for (int y = 0; y < 3; y++) {
            for (int z = 0; z < 2; z++)
                std::cout << m[x][y][z] << "\t";
            std::cout << "\n";
        }
        std::cout << "\n\n";
    }    
}

Context

StackExchange Code Review Q#117919, answer score: 4

Revisions (0)

No revisions yet.