3
\$\begingroup\$

The syntax for specifying multi-dimensional types in C++ is messy. For example, to specify a two-dimensional std::vector<int>, one has to write std::vector<std::vector<int>>. 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 <template <typename...> class T, typename Base, std::size_t n>
struct multi {
  static_assert(n > 0, "");
  using type = T<typename multi<T, Base, n - 1>::type>;
};

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

/// Provides easy syntax for specifying multi-dimensional types.
/// For example, `multi_t<std::vector, int, 2>` is equivalent to
/// `std::vector<std::vector<int>>`.
template <template <typename...> class T, typename Base, std::size_t n>
using multi_t = typename multi<T, Base, n>::type;
\$\endgroup\$
  • \$\begingroup\$ A question about intent: is multi_t<std::vector, int, 2> easier to understand than std::vector<std::vector<int>>? To me it's not and for something like multi_t<std::vector, int, 4> more often than not the problem is you need it naked (instead of using proper classes). But that's just my opinion. \$\endgroup\$ – Adriano Repetti Jan 26 '16 at 10:08
  • \$\begingroup\$ @AdrianoRepetti You got a point. But this is just the cleanest syntax I can come up with at present. Do you have any suggestions as to what the syntax should be like? \$\endgroup\$ – Lingxi Jan 26 '16 at 10:41
  • \$\begingroup\$ @AdrianoRepetti I think multi_t<std::vector<int>, 2> is better syntax. But this will place an additional requirement on the template, e.g., have a member type named value_type. \$\endgroup\$ – Lingxi Jan 26 '16 at 10:43
  • \$\begingroup\$ honestly I don't have. For simple case (2) I'm happy with current syntax (well, of course it's just my own opinion!) and for higher dimensions I'd introduce classes to describe what is what. Yes, sometimes I may just need std::vector<std::vector<std::vector<int>>> and it's pretty weird but (so far!) I didn't have this need. \$\endgroup\$ – Adriano Repetti Jan 26 '16 at 10:45
4
\$\begingroup\$

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 <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);
    }
};

Simple demo code:

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";
    }    
}

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<n-1>, 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...

|improve this answer|||||
\$\endgroup\$
  • \$\begingroup\$ If you used std::initializer_list for the constructor, you could make it so that the number of dimensions is derived from the size of the initializer list restoring that capability of the original to have the number of dimensions configurable. Store the dimensions in a std::vector and you'd even be able to alter dimensionality at runtime. \$\endgroup\$ – Edward Jan 26 '16 at 22:21
  • \$\begingroup\$ I like the idea of having a linear one-dimensional container but with multi-dimensional indices into it. But I don't want to introduce new container types, e.g., matrix3 in your code. That would be too heavy. I think I'm more in favor of a light-weight wrapper class (something like std::reference_wrapper) that provides multi-dimensional indexing into a linear container. \$\endgroup\$ – Lingxi Jan 27 '16 at 2:37
  • \$\begingroup\$ @Lingxi: Fair enough--this is just some code I've had lying around for years that seemed like it might provide some...inspiration for things you might want to consider, not really suggesting that you try to use it directly. \$\endgroup\$ – Jerry Coffin Jan 27 '16 at 3:01
  • \$\begingroup\$ @JerryCoffin Inspired by your comments, I've just made a class template to access one-dimensional objects in a multi-dimensional fashion :-) See here. \$\endgroup\$ – Lingxi Jan 27 '16 at 16:19

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.