c++arraysmultidimensional-arraystdarray

Convenient way to declare 2D (or even higher dimension) arrays with std::array


I'm about to convert a lot of old C++ code to more modern C++.

There are many raw 2D arrays in that code like:

Foo bar[XSIZE][YSIZE];

And I'm about to replace these declarations with

std::array<std::array<Foo, YSIZE>, XSIZE> bar;

This is a convenient way because the statements stay the same and the code is supposed to behave the same as with raw arrays with the additional benefit of being able to have out of bounds checks in debug builds.

But IMO the std::array<std::array<Foo, YSIZE>> is somewhat cumbersome and not easy to read, and with 3D arrays (although I have none) it would even be worse.

Right now I'm using this macro to make the declaration more readable:

#define DECLARE_2D_ARRAY(type, x, y) std::array<std::array<type, y>, x>
...
DECLARE_2D_ARRAY(Foo, XSIZE, YSIZE) bar;

But I feel this to be a macro hack, and I'm wondering if there is a cleaner, more C++ way to do something similar.


Solution

  • template<class A>
    struct std_array_helper {
      using type=A;
    };
    
    template<class A>
    using array_t = typename std_array_helper<A>::type;
    
    template<class T, std::size_t N0>
    struct std_array_helper<T[N0]> {
      using type=std::array<array_t<T>, N0>;
    };
    

    now

    array_t<Foo[XSIZE][YSIZE]>
    

    is

    std::array< std::array<Foo, XSIZE>, YSIZE>
    

    an alternative solutions is:

    template<class T, std::size_t...Sz>
    struct array_helper {
      using type=T;
    };
    
    template<class T0, std::size_t...Ns>
    using array_t = typename array_helper<T0, Ns...>::type;
    
    template<class T, std::size_t N0, std::size_t...Ns>
    struct array_helper<T, N0, Ns...>
    {
      using type=std::array<array_t<T, Ns...>, N0>;
    };
    

    this uses the syntax:

    array_t<Foo, XSIZE, YSIZE>
    

    if you prefer it.

    We can even combine the two, allowing either syntax.

    template<class T, std::size_t...Sz>
    struct array_helper {
      using type=T;
    };
    template<class T0, std::size_t...Ns>
    using array_t = typename array_helper<T0, Ns...>::type;
    
    template<class T, std::size_t N0, std::size_t...Ns>
      requires (!std::is_array_v<T>)
    struct array_helper<T, N0, Ns...>
    {
      using type = std::array<array_t<T, Ns...>, N0>;
    };
    
    template<class T, std::size_t N0, std::size_t...Ns>
    struct array_helper<T[N0], Ns...>:
      array_helper<array_t<T, Ns...>, N0>
    {};
    

    and now

    array_t< Foo[XSIZE], YSIZE >
    

    works.

    But be careful - the order is tricky!

    int[3][2] is an array of 3 elements of arrays of 2 elements.
    

    To keep this the same we want

    array_t<int, 3, 2>
    

    to be

    std::array< std::array< int, 2 >, 3>
    

    not

    std::array< std::array< int, 3 >, 2>
    

    here are test cases to determine if you got the order right:

    static_assert( std::is_same_v< std::array<int, 3>, array_t<int, 3> > );
    static_assert( std::is_same_v< std::array< std::array<int, 2>, 3>, array_t<int, 3, 2> > );
    static_assert( std::is_same_v< std::array< std::array<int, 2>, 3>, array_t<int[3], 2> > );
    static_assert( std::is_same_v< std::array< std::array<int, 2>, 3>, array_t<int[3][2]> > );
    

    remove whichever have the wrong syntax for your chosen array_t.

    Live example

    Now, even this might be wrong. It feels incorrect that

    array_t<int[3], 2>
    

    doesn't have sub-arrays of size 3, yet

    array_t<int[3][2]>
    

    feels like it should also be the same array, and the layout of int[3][2] should agree with array_t<int[3][2]> and agree with array_t<int, 3, 2>.

    Also, array_t< array_t<int, 3>, 2> should be the same as array_t<int[3], 2>.

    These requirements disagree with each other. I mean, all over the place they disagree.

    Probably the simplest way to resolve this is to require only [][][] syntax, or don't permit mixed [] and , syntax.

    Having array_t<int[3][2]> with the same layout as int[3][2] is high value. Similarly, having array_t< int, 3, 2 > syntax is high value. Probably we want array_t<int, 3, 2> to mean the same as int[3][2]? Throw away this being equal to array_t< array_t<int, 3>, 2> - instead it equals array_t<array_t<int,2>,3>. Finally, block array_t<int[3], 2> syntax as confusing.

    Then, split the array_t< T, 1,2,3,...> from array_t<T[1][2][3]...> templates to minimize confusion.