Declaring 2 (or even multi-) dimensional std::arrays elegantly

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]


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.


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.

Tags:

C++

Stdarray