14

I'm using 2-dimensional arrays based on std::array.

Basically instead of:

MyType myarray[X_SIZE][Y_SIZE];

I have:

std::array<std::array<MyType, Y_SIZE>, X_SIZE> myarray;

This works perfectly fine but IMO the declaration is not very readable.

Is there a way to declare this using some clever C++ template mecanism, so the declaration could look something like this?

My2DArray<Mytype, X_SIZE, Y_SIZE> myarray;
xlm
  • 6,854
  • 14
  • 53
  • 55
Jabberwocky
  • 48,281
  • 17
  • 65
  • 115
  • 2
    I don't think code elegance was/is a priority of 'Modern' C++! – Adrian Mole Sep 02 '19 at 14:25
  • 1
    I would expect it to be `[Y][X]` (and `std::array, Y>` in the template in the answer), as usually I think about X being the base dimension, which is iterated mostly continuously.. (of course if you iterate mostly through Y, then your example is optimal too, so it depends on the usage) – Ped7g Sep 02 '19 at 14:33
  • @Ped7g no, X,Y rather than Y,X is on purpose because accessing is done like this: `myarray[x_index][y_index]`. – Jabberwocky Sep 02 '19 at 14:35
  • @Jabberwocky that's what I am talking about, if it's for example `int map[4][3];` then the memory layout is `[0][0], [0][1], [0][2], [1][0], [1][1], ...`, i.e. the second dimension form continuous blocks of integers, so you should (when possible) iterate over second dimension in the inner loop, and first dimension in the outer loop. In your dimension that means to have X as outer loop and Y inner (I mean for cases where this distinction is even possible, like adding of matrices, in multiplying it doesn't matter that much, as one of the two matrices has to be traversed in suboptimal order), etc – Ped7g Sep 02 '19 at 15:01
  • @Ped7g it's not about optimisation here but solely about code readability. But you are right. – Jabberwocky Sep 02 '19 at 15:04

3 Answers3

22

If you want just 2D arrays, it's fairly straightforward:

template <class T, std::size_t X, std::size_t Y>
using My2DArray = std::array<std::array<T, Y>, X>;

If you want a generic mechanism not limited to 2D arrays, it can be done too:

template <class T, std::size_t N, std::size_t... Ns>
struct AddArray {
    using type = std::array<typename AddArray<T, Ns...>::type, N>;
};

template <class T, std::size_t N>
struct AddArray<T, N> {
    using type = std::array<T, N>;
};

template <class T, std::size_t... N>
using MyNDArray = typename AddArray<T, N...>::type;

[Live example]

Angew is no longer proud of SO
  • 167,307
  • 17
  • 350
  • 455
4

A somewhat elegant way to implement this operation is with a fold expression:

// Some namespace to hide the poorly-constrained template function:
namespace array_making {
    template <std::size_t N>
    struct array_dim {};

    template <typename T, std::size_t N>
    constexpr auto operator%(array_dim<N>, T const&)
        -> std::array<T, N>;
}

template <typename T, std::size_t... Is>
using md_array_t = decltype(
    (array_making::array_dim<Is>{} % ... % std::declval<T>())
);

Compiler Explorer.

Then md_array_t<int, 1, 2, 3> is array<array<array<int, 3>, 2>, 1>. If you prefer the opposite order, reverse the parameters of the operator% and the arguments to the fold expression.


Note that this will run into problems if the type T has an unconstrained operator% in an associated namespace (please constrain your operators!). We can reduce the risk of this happening by choosing unlikely operators such as .*, ->*, or %=; or we can use an array_type<T> wrapper. Neither solution completely avoids the problem of improperly constrained operator overloads for T.

Justin
  • 24,288
  • 12
  • 92
  • 142
3

We can wrap one of the existing MyNDArray / md_array_t answers to arrive at an alternative interface:

template <typename Arr, std::size_t... Is>
constexpr auto make_array_impl(std::index_sequence<Is...>)
    -> md_array_t<std::remove_all_extents_t<Arr>,
        std::extent_v<Arr, Is>...>;

template <typename Arr>
using make_array = decltype(make_array_impl<Arr>(
    std::make_index_sequence<std::rank_v<Arr>>{}));

Compiler Explorer

This allows us to write make_array<int[4][5][6]> to mean array<array<array<int, 6>, 5, 4>.


Explanation:

  1. std:rank gives the number of dimensions of an array type. Thus, for int[4][5][6], it returns 3.
  2. We hand this to make_index_sequence to end up with a pack of indices. (0, 1, 2)
  3. std::remove_all_extents gives us the underlying type of the array; T[a][b]...[n] -> T (int)
  4. std::extent gives us the extent of the given dimension. We call this for each index. (4, 5, 6).

By passing these to our previously-implemented md_array_t, we end up with md_array_t<int, 4, 5, 6>, which produces what we want.

Justin
  • 24,288
  • 12
  • 92
  • 142