How to make a function or constructor that accepts array of any rank

Viewed 123

I have a wrapper struct around a vector and a shape, like this:

template <std::size_t N>
struct array_type {
    std::array<std::size_t, N> shape;
    std::vector<float> data;
};

I would like to be able to construct an array_type from any array float[N], float[N][M], etc.

Currently, I have a function for each rank, e.g.

template <std::size_t N>
array_type<1> _1d(const deel::formal::float_type (&values)[N]) {
    return {{N},
            std::vector<float_type>(
                reinterpret_cast<const float_type*>(values),
                reinterpret_cast<const float_type*>(values) + N)};
}

template <std::size_t N>
array_type<2> _2d(const deel::formal::float_type (&values)[N][M]) {
    return {{N, M},
            std::vector<float_type>(
                reinterpret_cast<const float_type*>(values),
                reinterpret_cast<const float_type*>(values) + N * M)};
}

I would like to write something like:

template <class Array>
array_type<std::rank_v<Array>> make_array(Array const&);

...but that does not work for initializer list:

auto arr1 = _1d({1, 2, 3}); // Ok
auto arr2 = make_array({1, 2, 3}); // Ko

Is there a way to have this make_array? Or (since I think it's not possible), to have at least something like make_array<1>({1, 2, 3}) where the rank is explicitly specified?

3 Answers

array_type::shape can be generated with std::extent with a bit of template meta programming:

template<typename Array, std::size_t... I>
auto extents_impl(const Array& a, std::index_sequence<I...>)
{
    return std::array{std::extent_v<Array, I>...};
}

template<typename Array>
auto extents(const Array& a)
{
    return extents_impl(a, std::make_index_sequence<std::rank_v<Array>>());
}

With this, make_array can be written as:

template <class Array, std::enable_if_t<std::is_same_v<std::remove_cv_t<std::remove_all_extents_t<Array>>, float>, int> = 0> // magic incantation :)
array_type<std::rank_v<Array>> make_array(Array const& a)
{
    array_type<std::rank_v<Array>> ret {
        .shape = extents(a),
        .data = std::vector<float>(sizeof a / sizeof(float)),
    };
    std::memcpy(ret.data.data(), &a, sizeof a);
    return ret;
}

I use memcpy to avoid potential pointer type aliasing violations and the technicality of iterating outside the bounds of sub array being UB according to strict interpretation of the standard.

...but that does not work for initializer list:

Add one overload:

template <std::size_t N>
array_type<1> make_array(float const (&a)[N])
{
    return make_array<float[N]>(a);
}

Alternatively, you can specify the array type in the call:

make_array<float[2][3]>({{1, 2, 3},{4, 5, 6}});

This requires no overloads.

T (&&)[N] is suggested when you want a function template to accept a braced-init-list and deduce its length, furthermore, you can use T (&&)[M][N], T (&&)[M][N][O] ... to deduce the lengths of each rank (sadly, there is nothing like T (&&)[N]...[Z] to accept array with arbitrary rank).

so you can provide overloading function like this:


// attention that `T (&&)[N][M]` is `Y (&&)[N]` where `Y` is `T[M]`.

template<typename T, size_t N>
using Array1D_t = T[N];
template<typename T, size_t M, size_t N>
using Array2D_t = Array1D_t<Array1D_t<T, N>, M>;
template<typename T, size_t M, size_t N, size_t O>
using Array3D_t = Array1D_t<Array2D_t<T, N, O>, M>;
// ...

// assume the max rank is X.

template<typename T, size_t N>
array_type<1> make_array(Array1D_t<T, N>&& v);
template<typename T, size_t M, size_t N>
array_type<2> make_array(Array2D_t<T, M, N>&& v);
template<typename T, size_t M, size_t N, size_t O>
array_type<3> make_array(Array3D_t<T, M, N, O>&& v);
// ...

and then, it will be okay for make_array({1, 2, 3}), make_array({{1, 2, 3}, {4, 5, 6}}) ..., except the rank is larger than X.

Is there a way to have this make_array?

I don't think so (but, to be honest, I'm not able to demonstrate it's impossible).

Or (since I think it's not possible), to have at least something like make_array<1>({1, 2, 3}) where the rank is explicitly specified?

I don't see a way also in this case.

But... if you accept that the sizes are explicitly specified... you can write a recursive custom type traits to construct the final type

// declararion and ground case
template <typename T, std::size_t ...>
struct get_ranked_array
 { using type = T; };

// recursive case
template <typename T, std::size_t Dim0, std::size_t ... Dims>
struct get_ranked_array<T, Dim0, Dims...>
   : public get_ranked_array<T[Dim0], Dims...>
 { };

template <typename T, std::size_t ... Dims>
using get_ranked_array_t = typename get_ranked_array<T, Dims...>::type;

and the make function simply become

template <std::size_t ... Dims>
auto make_ranked_array (get_ranked_array_t<float, Dims...> const & values)
   -> array_type<sizeof...(Dims)>
 {
   return {{Dims...},
      std::vector<float>(
         reinterpret_cast<float const *>(values),
         reinterpret_cast<float const *>(values) + (Dims * ...))};
 }

You can use it as follows

auto arr1 = make_ranked_array<3u>({1.0f, 2.0f, 3.0f}); 
auto arr2 = make_ranked_array<3u, 2u>({ {1.0f, 2.0f, 3.0f},
                                        {4.0f, 5.0f, 6.0f} }); 

If you want, you can maintain the deduction of the last size, but I don't know if it's a good idea.

The following is a full compiling C++17 example

#include <array>
#include <vector>

template <std::size_t N>
struct array_type
 {
   std::array<std::size_t, N> shape;
   std::vector<float> data;
 };

// declararion and ground case
template <typename T, std::size_t ...>
struct get_ranked_array
 { using type = T; };

// recursive case
template <typename T, std::size_t Dim0, std::size_t ... Dims>
struct get_ranked_array<T, Dim0, Dims...>
   : public get_ranked_array<T[Dim0], Dims...>
 { };

template <typename T, std::size_t ... Dims>
using get_ranked_array_t = typename get_ranked_array<T, Dims...>::type;

template <std::size_t ... Dims>
auto make_ranked_array (get_ranked_array_t<float, Dims...> const & values)
   -> array_type<sizeof...(Dims)>
 {
   return {{Dims...},
      std::vector<float>(
         reinterpret_cast<float const *>(values),
         reinterpret_cast<float const *>(values) + (Dims * ...))};
 }

int main ()
 {
   auto arr1 = make_ranked_array<3u>({1.0f, 2.0f, 3.0f}); 
   auto arr2 = make_ranked_array<3u, 2u>({ {1.0f, 2.0f, 3.0f},
                                           {4.0f, 5.0f, 6.0f} }); 

   static_assert ( std::is_same_v<decltype(arr1), array_type<1>> );
   static_assert ( std::is_same_v<decltype(arr2), array_type<2>> );
 }
Related