Search code examples
c++arraysvariadic-templatesc++20

C++20 Deducing the dimensions of a multidimensional array into a parameter pack


I am implementing a container in the form of a multidimensional array. I am trying to replicate std::to_array (C++ 20), a function that creates a std::array from a one dimensional built-in array, using my multidimensional array:

template<typename T, std::size_t... N>
constexpr mdarray<std::remove_cv_t<T>, N...> to_mdarray(T (&a)[N]));

int arr[5][12]{};
to_mdarray(arr); // Returns mdarray<int, 5, 12>

I would like to deduce the dimensions of the built-in array into N and return the corresponding mdarray. I have not managed to figure out the correct syntax, I've always ended up with errors. Either I am not unfolding the pack properly, or the compiler thinks I am trying to declare a function pointer or lambda. I haven't found any resources online either.

I found two alternatives so far. One is to decay the array into a pointer and let the user specify the dimensions.

template<typename T, std::size_t... N>
constexpr mdarray<std::remove_cv_t<T>, N...> to_mdarray(T* const a);

int arr[5][12]{};
to_mdarray<5, 12>(arr);

I found the other in blog from Stanislav Arnaudov. It involves generating N-th dimensional overloads using recursive macros:

#define SIZE_T_S_1 size_t N
#define SIZE_T_S_2 SIZE_T_S_1 , size_t M
#define SIZE_T_S_3 SIZE_T_S_2 , size_t L

#define BRACKETS_S_1 [N]
#define BRACKETS_S_2 BRACKETS_S_1[M]
#define BRACKETS_S_3 BRACKETS_S_2[L]

#define LETTERS_S_1 N
#define LETTERS_S_2 LETTERS_S_1, M
#define LETTERS_S_3 LETTERS_S_2, L

#define TO_MD_N(dim) template<typename T, SIZE_T_S_##dim>                                       \
constexpr mdarray<std::remove_cv_t<T>, LETTERS_S_##dim> to_mdarray(T (&values) BRACKETS_S_##dim);

TO_MD_N(1);
TO_MD_N(2);
TO_MD_N(3);

// Equivalent to:
// TO_MD_N(1)
template<typename T, std::size_t N>
constexpr mdarray<std::remove_cv_t<T>, N> to_mdarray(T (&values)[N]);

// TO_MD_N(2)
template<typename T, std::size_t, std::size_t M>
constexpr mdarray<std::remove_cv_t<T>, N, M> to_mdarray(T (&values)[N][M]);

// TO_MD_N(3)
template<typename T, std::size_t, std::size_t M, std::size_t L>
constexpr mdarray<std::remove_cv_t<T>, N, M, L> to_mdarray(T (&values)[N][M][L]);

Both of these solutions have flaws. One requires the user to enter the correct dimensions, the other requires defining the same function multiple times and restricting the number of dimensions.

Is there any way to do this in C++20 or is it impossible at the moment?


Solution

  • Unfortunately my usual trend of Boost.Mp11 one-liners will come to an end here, since Boost.Mp11 doesn't really deal with values very well and we need to convert int[5][12] into mdarray<int, 5, 12> and there's no easy mechanism to do that with that library.

    Instead we just do it by hand. std::extent gives you the Nth extent and std::rank gives you the number of extents. Combine that with make_index_sequence and you can get all of 'em:

    template <typename T, typename>
    struct mdarray_for_impl;
    
    template <typename A, size_t... Is>
    struct mdarray_for_impl<A, std::index_sequence<Is...>> {
        using type = mdarray<std::remove_all_extents_t<A>, std::extent_v<A, Is>...>;
    };
    
    template <typename T>
    using mdarray_for = mdarray_for_impl<T, std::make_index_sequence<std::rank_v<T>>>::type;
    

    Here mdarray_for<int[1][2][3]> will yield the type mdarray<int, 1, 2, 3>.


    A Boost.Mp11 version anyway requires a couple helper aliases

    // a type-only mdarray
    template <typename T, typename... Ns>
    using mdarray_t = mdarray<T, Ns::value...>;
    
    // a type-only extent
    template <typename T, typename V>
    using mp_extent = mp_size_t<std::extent_v<T, V::value>>;
    

    which then allows:

    template <typename T>
    using mdarray_for2 = 
        mp_apply<mdarray_t,
        mp_append<
            mp_list<std::remove_all_extents_t<T>>,
            mp_transform_q<
                mp_bind<mp_extent, T, _1>,
                mp_iota_c<std::rank_v<T>>>
            >>;
    

    This is the same algorithm as before, except mp_iota_c<std::rank_v<T>> to get the sequence of extent indices (instead of std::make_index_sequence) and then mp_transform_q to get the nth extent (instead of using it directly in a pack expansion).

    In this case for int[5][12] we build up mp_list<int, mp_size_t<5>, mp_size_t<12>> (because we're all types, no values) and then mp_apply turns that into mdarray_t<...> which turns it into mdarray<int, 5, 12>.