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::array
s 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?
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.
#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;
}