c++variadic-templatesstdarray

How can I flatten a parameter pack of collections into a member initialized std::array?


Edit 2024-05-07

I have solved my problem. Thank you for all the input. I have successfully managed to implement a "collection splatter." Here is the code that does this:

#include <type_traits>
#include <concepts>
#include <cstdint>
#include <tuple>
#include <array>

using std::size_t;

struct array_marker {};

template <typename T>
concept IsTupleLike = requires { std::tuple_size<typename std::remove_cvref<T>::type>::value; };

template <typename T>
concept IsArrayMarker = std::same_as<std::remove_cvref_t<T>, array_marker>;

template <typename T>
concept IsPlain = !IsArrayMarker<T> && !IsTupleLike<T>;

template <typename... Ts>
struct tuple_data {
        static constexpr size_t size = (tuple_data<Ts>::size + ...);
        using value_type = std::common_type_t<typename tuple_data<Ts>::value_type...>;
};

template <typename T>
struct tuple_data<T> {
        static constexpr size_t size = 1;
        using value_type = std::remove_cvref_t<T>;
};

template <IsTupleLike T>
struct tuple_data<T> {
        static constexpr size_t size = std::tuple_size_v<std::remove_cvref_t<T>>;
        using value_type = std::remove_cvref_t<T>::value_type;
};

template <typename Ret, IsPlain Arg, typename... Args>
constexpr inline Ret make_collection(Arg &&arg, Args &&... args) {
        return make_collection<Ret>(std::forward<Args>(args)..., std::forward<Arg>(arg));
}

template <typename Ret, IsTupleLike Tup, typename... Args, size_t... Indices>
constexpr inline Ret make_collection(std::index_sequence<Indices...> &&, Tup &&arr, Args &&... args) {
        return make_collection<Ret>(std::forward<Args>(args)..., get<Indices>(arr)...);
}

// this needs to match ANYTHING of std::array
template <typename Ret, IsTupleLike Tup, typename... Args>
constexpr inline Ret make_collection(Tup &&arr, Args &&... args) {
        return make_collection<Ret>(std::make_index_sequence<tuple_data<Tup>::size>{}, std::forward<Tup>(arr), std::forward<Args>(args)...);
}

template <typename Ret, typename... Args>
constexpr inline Ret make_collection(IsArrayMarker auto &&, Args &&... args) {
        return { args... };
}

template <template <typename, size_t> typename ArrayLike = std::array,
        typename... Args,
        typename Ret = ArrayLike<typename tuple_data<Args...>::value_type, tuple_data<Args...>::size>>
constexpr inline Ret make_array(Args &&... args) {
        return make_collection<Ret>(std::forward<Args>(args)..., array_marker{});
}

The make_collection function is the implementer function for make_array, and can be easily used to generate things other than a std::array (std::vector maybe). The reason why I have an ArrayLike parameter in the make_array function is to be able to create other structures that have a template signature similar to std::array. I made this structure as an example:

#include <array>
#include <iostream>

template <typename Member, size_t Size>
struct group {
    using value_type = Member;

    constexpr group() : arr{} {}
    
    constexpr group(auto &&... args) : arr(make_array(args...)) {}

    std::array<Member, Size> arr;
};

group(auto &&... args) -> group<typename tuple_data<decltype(args)...>::value_type, tuple_data<decltype(args)...>::size>;

template <typename Member, size_t Size>
struct std::tuple_size<group<Member, Size>> : std::integral_constant<std::size_t, Size> {};

template <size_t Index, typename T, size_t Size>
T &get(group<T, Size> &g) {
    return g.arr[Index];
}

template <size_t Index, typename T, size_t Size>
const T &get(const group<T, Size> &g) {
    return g.arr[Index];
}

template <size_t Index = 0>
static inline std::ostream &operator<<(std::ostream &os, IsTupleLike auto &&tup) {
    constexpr const size_t size = std::tuple_size_v<std::remove_cvref_t<decltype(tup)>>;
    if constexpr (Index == 0) {
        return operator<<<Index + 1>(os << "{", tup) << "}";
    } else if constexpr (Index > size) {
        return os;
    } else if constexpr (Index == 1) {
        return operator<<<2>(os << get<0>(tup), tup);
    } else {
        return operator<<<Index + 1>(os << ", " << get<Index - 1>(tup), tup);
    }
}

static inline void println(auto &&... args) {
    ((std::cout << args << " "), ...) << "\n";
}

int main() {
    auto g1 = make_array<group>(1, 2, 3);
    auto g2 = make_array<group>(g1, 4, 5, 6);
    group g3{g1, 0, g2};
    println(g1, g2, g3);
}

Output:

{1, 2, 3} {1, 2, 3, 4, 5, 6} {1, 2, 3, 0, 1, 2, 3, 4, 5, 6}

This code, of course, requires that the first code block be included into the second (possibly as a header file).

Edit 2024-05-06

I have found a way to generate a std::array from a parameter pack of std::arrays and other things. It is fairly close to what I am looking for. I now only need to find how to incorporate the custom class group into this.

Updated Code:

#include <cstdint>
#include <iostream>
#include <concepts>
#include <type_traits>
#include <utility>
#include <tuple>
#include <array>

using std::size_t;

template <typename T>
struct array_marker {};

template <typename T>
concept IsTupleLike = requires { std::tuple_size<typename std::remove_cvref<T>::type>::value; };

template <typename T, typename U>
concept IsArrayMarker = std::same_as<std::remove_cvref_t<T>, array_marker<U>>;

template <typename T, typename U>
concept IsPlainOf = !IsArrayMarker<T, U> && !IsTupleLike<T>;

template <typename T>
struct tuple_data {
    static constexpr const size_t size = 1;
    using type = T;
};

template <typename T, size_t S>
struct tuple_data<std::array<T, S>> {
    static constexpr const size_t size = S;
    using type = T;
};

template <IsTupleLike T>
struct tuple_data<T> {
    static constexpr const size_t size = std::tuple_size_v<std::remove_cvref_t<T>>;
    using type = std::remove_cvref_t<T>::value_type;
};

template <typename T, size_t S, IsPlainOf<T> Arg, typename... Args>
constexpr inline std::array<T, S> make_array_i(Arg &&arg, Args &&... args) {
    return make_array_i<T, S>(std::forward<Args>(args)..., std::forward<Arg>(arg));
}

template <typename T, size_t S, IsTupleLike Tup, typename... Args, size_t... Indices>
constexpr inline std::array<T, S> make_array_i(std::index_sequence<Indices...> &&, Tup &&arr, Args &&... args) {
    return make_array_i<T, S>(std::forward<Args>(args)..., get<Indices>(arr)...);
}

// this needs to match ANYTHING of std::array
template <typename T, size_t S, IsTupleLike Tup, typename... Args>
constexpr inline std::array<T, S> make_array_i(Tup &&arr, Args &&... args) {
    return make_array_i<T, S>(std::make_index_sequence<tuple_data<Tup>::size>{}, std::forward<Tup>(arr), std::forward<Args>(args)...);
}

template <typename T, size_t S, typename... Args>
constexpr inline std::array<T, S> make_array_i(IsArrayMarker<T> auto &&, Args &&... args) {
    return { args... };
}

template <typename... Args, typename T = std::common_type_t<typename tuple_data<Args>::type...>,
    size_t S = (tuple_data<Args>::size + ...)>
constexpr inline std::array<T, S> make_array(Args &&... args) {
    return make_array_i<T, S>(std::forward<Args>(args)..., array_marker<T>{});
}

template <typename T, size_t N>
std::ostream &operator<<(std::ostream &os, const std::array<T, N> &arr) {
    os << "arr<" << N << ">{";
    for (int i = 0; i < N; i++) {
        if (i != 0) {
            os << ", ";
        }
        os << arr[i];
    }
    return os << "}";
}

int main() {
    auto arr1 = make_array(1, 2);
    const auto arr2 = make_array(4, 5);
    auto arr = make_array(arr2, 1, arr1, arr1);
    std::cout << arr << "\n";
}

This uses concepts, so it will not work for versions C++17 and below. However, because of those concepts, I think it is decently generic, so it could work for things other than std::array, but I haven't tested it yet. All it requires is that std::tuple_size and get<Index>(Class) is defined for the class used. I believe the answer marked as correct will work for those not using C++20+.

Original 2024-05-05

I want to find a way to write the following:

group a{1, 2};
group b{a, 3}; // synonym to group b{1, 2, 3}

I have gotten to an environment where this only partially works, with the base case of plain parameters working:

#include <cstdint>
#include <array>
#include <iostream>

template <typename Member, size_t Dim>
struct group;

template <typename... Args>
static constexpr inline std::array<std::common_type_t<Args...>, sizeof...(Args)> make_array
(Args &&... args) {
    return { args... };
}

template <size_t VDim, size_t... Indices>
static constexpr inline auto make_array_i
(auto &&... args_begin, std::index_sequence<Indices...>, const group<auto, VDim> &v, auto &&... args_end) {
    return make_array(args_begin..., v[Indices]..., args_end...);
}

template <size_t VDim>
static constexpr inline auto make_array
(auto &&... args_begin, const group<auto, VDim> &v, auto &&... args_end) {
    return make_array_i(args_begin..., std::make_index_sequence<VDim>{}, v, args_end...);
}

template <typename Member, size_t Dim>
struct group {
    template <typename... Args>
    constexpr group(Args &&... args) : dat(make_array(args...)) {}

    constexpr const Member &operator[](const size_t d) const {
        return dat[d];
    }

private:
    std::array<Member, Dim> dat;
};

int main() {
    group<int, 2> a{1, 2};
    // group<int, 3> b{1, a};
}

However, the code does not compile for group b object, with the message array must be initialized with a brace-enclosed initializer. I am confused, since it is essentially doing the same thing in the group a case, but behaves perfectly fine with that.

I have also tried double bracketing the args in the first make_array overload ({{ args... }}), but this does not seem to work either, with same issue.

I cannot move away from std::array, and I would prefer to keep everything constexpr. This is taken from a larger piece of code, hence the static definitions of the methods.

Here is the full error message for those who are curious:

min.cpp: In instantiation of ‘constexpr group<Member, Dim>::group(Args&& ...) [with Args = {int, group<int, 2>&}; Member = int; long unsigned int Dim = 3]’:
min.cpp:41:22:   required from here
min.cpp:29:58: error: array must be initialized with a brace-enclosed initializer
   29 |         constexpr group(Args &&... args) : dat(make_array(args...)) {}
      |                                                ~~~~~~~~~~^~~~~~~~~
min.cpp: In instantiation of ‘constexpr group<Member, Dim>::group(Args&& ...) [with Args = {int&}; Member = int; long unsigned int Dim = 2]’:
min.cpp:11:19:   required from ‘constexpr std::array<typename std::common_type<_Tp>::type, sizeof... (Args)> make_array(Args&& ...) [with Args = {int&, group<int, 2>&}; typename std::common_type<_Tp>::type = group<int, 2>]’
min.cpp:29:51:   required from ‘constexpr group<Member, Dim>::group(Args&& ...) [with Args = {int, group<int, 2>&}; Member = int; long unsigned int Dim = 3]’
min.cpp:41:22:   required from here
min.cpp:29:58: error: array must be initialized with a brace-enclosed initializer
min.cpp: In instantiation of ‘constexpr group<Member, Dim>::group(Args&& ...) [with Args = {group<int, 2>&}; Member = int; long unsigned int Dim = 2]’:
min.cpp:11:19:   required from ‘constexpr std::array<typename std::common_type<_Tp>::type, sizeof... (Args)> make_array(Args&& ...) [with Args = {int&, group<int, 2>&}; typename std::common_type<_Tp>::type = group<int, 2>]’
min.cpp:29:51:   required from ‘constexpr group<Member, Dim>::group(Args&& ...) [with Args = {int, group<int, 2>&}; Member = int; long unsigned int Dim = 3]’
min.cpp:41:22:   required from here
min.cpp:29:58: error: array must be initialized with a brace-enclosed initializer

As a side note, is the group a object using a copy constructor for initialization? Is there some strange copy elision magic going on? Is this related to the error message that I am getting?


Solution

  • Here's an implementation that works, is constexpr, doesn't use recursion and properly forwards elements, which allows move-only types to be used. The element type must be default constructible, which isn't strictly necessary but is a huge pain to fix.

    Effort was made to write this without using C++20, with the exception that you'll need to backport std::span. Inheriting from std::span is a bit questionable, but it should be mostly fine and I'm too lazy to rewrite that.

    Live

    #include<array>
    #include<span>
    #include<algorithm>
    
    using std::size_t;
    
    template<typename T, typename = void>
    struct value_type
    {
        using type = T;
    };
    
    template<typename T>
    struct value_type<T, std::void_t<typename T::value_type>>
    {
        using type = typename T::value_type;
    };
    
    template<typename T>
    using value_type_t = typename value_type<T>::type;
    
    template<typename, size_t>
    struct group;
    
    template<typename T>
    struct size
    {
        static constexpr size_t value = 1;
    };
    
    template<typename T, size_t N>
    struct size<std::array<T, N>>
    {
        static constexpr size_t value = N;
    };
    
    template<typename T, size_t N>
    struct size<group<T, N>>
    {
        static constexpr size_t value = N;
    };
    
    template<typename T>
    inline constexpr auto size_v = size<T>::value;
    
    template<typename T>
    struct move_span : std::span<T>
    {
        using base = std::span<T>;
        using base::span;
    
        constexpr auto begin() const noexcept
        {
            return std::make_move_iterator(base::begin());
        }
    
        constexpr auto end() const noexcept
        {
            return std::make_move_iterator(base::end());
        }
    };
    
    template<typename T>
    constexpr std::span<const T> make_span(const T& t)
    {
        return {&t, 1};
    }
    
    template<typename T, std::enable_if_t<!std::is_reference_v<T>, int> = 0>
    constexpr move_span<T> make_span(T&& t)
    {
        return {&t, 1};
    }
    
    template<typename T, size_t N>
    constexpr std::span<const T> make_span(const std::array<T, N>& arr)
    {
        return arr;
    }
    
    template<typename T, size_t N>
    constexpr move_span<T> make_span(std::array<T, N>&& arr)
    {
        return arr;
    }
    
    template<typename T, size_t N>
    struct group
    {
        using value_type = T;
    
        struct span_tag_t {};
    
        template<typename... Ts>
        constexpr group(Ts&&... ts)
        : group{span_tag_t{}, make_span(std::forward<Ts>(ts))...}
        {
        }
    
        template<typename... Spans>
        constexpr group(span_tag_t, Spans... spans)
        {
            auto p = dat.data();
            ((p = std::copy(spans.begin(), spans.end(), p)), ...);
        }
    
        std::array<T, N> dat;
    };
    
    template<typename... Ts>
    group(Ts...)
    -> group<std::common_type_t<value_type_t<Ts>...>, (size_v<Ts> + ...)>;
    
    template<typename T, size_t N>
    constexpr move_span<T> make_span(group<T, N>&& g)
    {
        return g.dat;
    }
    
    template<typename T, size_t N>
    constexpr std::span<const T> make_span(const group<T, N>& g)
    {
        return g.dat;
    }