Consider the following C++23 code:
#include <tuple>
#include <cstdio>
#include <iostream>
struct W1
{
int n;
bool b;
W1(int x, bool f) : n(x), b(f) {}
};
struct W2
{
long n;
W2(long x) : n(x) {}
W2(const W2& other) : n(other.n) { std::puts("copy ctor"); }
W2(W2&& other) : n(other.n) { std::puts("move ctor"); }
};
int main() {
Foo<W1, W2, W2> foo{std::in_place_type<W1>, 42, true,
std::in_place_type<W2>, 7L,
std::in_place_type<W2>, 9};
std::cout << std::tuple_size_v<decltype(foo.data)> << '\n';
std::cout << get<0>(foo.data).n << '\n';
std::cout << get<0>(foo.data).b << '\n';
std::cout << get<1>(foo.data).n << '\n';
std::cout << get<2>(foo.data).n << '\n';
return 0;
}
Here, Foo
is a class template that has an std::tuple
data member data
(ignoring the encapsulation for the sake of simplicity), which in this case consists of three objects: W1{42, true}
, W2{7L}
and W2{9}
.
My question is: How to create class Foo
that conforms to the usage shown above?
(note that Foo
should be able to construct any number of objects of any type)
I have made it work like this:
#include <tuple>
template <typename>
inline constexpr bool is_in_place_type_v = false;
template <typename T>
inline constexpr bool is_in_place_type_v<std::in_place_type_t<T>> = true;
template <std::size_t Offset, std::size_t... Ints>
auto make_offset_sequence_impl(std::index_sequence<Ints...>) {
return std::index_sequence<Offset + Ints...>{};
}
template <std::size_t Offset, std::size_t N>
auto make_offset_sequence() {
return make_offset_sequence_impl<Offset>(std::make_index_sequence<N>());
}
// helper type that effectively allows for splitting a tuple into two
// , based on the position of `std::in_place_type`
template <int N, typename... Types>
struct Indexer
{};
// end-case
template <int N>
struct Indexer<N>
{
using arg_seq = decltype(std::make_index_sequence<N>());
using rest_seq = std::index_sequence<>;
using type = Indexer;
};
template <int N, typename Type, typename... Types>
struct Indexer<N, Type, Types...>
{
using type = Indexer<N + 1, Types...>::type;
};
template <int N, typename PT, typename... Types>
requires is_in_place_type_v<std::decay_t<PT>>
struct Indexer<N, PT, Types...>
{
using arg_seq = decltype(std::make_index_sequence<N>());
using rest_seq = decltype(make_offset_sequence<N, sizeof...(Types) + 1>());
using type = Indexer;
using next = Indexer<N, Types...>::type;
};
template <typename... Types>
struct Foo
{
std::tuple<Types...> data;
static auto m_init() { return std::tuple{}; }
template <typename T, typename... Ts>
static auto m_init(const std::in_place_type_t<T>&, Ts&&... args) {
using indices = Indexer<0, std::in_place_type_t<T>, Ts...>::next;
auto lambda1 = [&args...]<std::size_t... Is>(std::index_sequence<Is...>)
{
return std::tuple{
T{std::get<Is>(std::tuple{std::forward<Ts>(args)...})...}
};
};
auto lambda2 = [&args...]<std::size_t... Is>(std::index_sequence<Is...>) {
return m_init(std::get<Is>(std::tuple{std::forward<Ts>(args)...})...);
};
return std::tuple_cat(lambda1(typename indices::arg_seq{}),
lambda2(typename indices::rest_seq{}));
}
template <typename... Ts>
explicit Foo(Ts&&... ts) : data(m_init(std::forward<Ts>(ts)...)) {}
};
However, this produces more copies/moves than a regular constructor (used like Foo foo{W1{42, true}, W2{7L}, W2{9}};
) would, thus defeating the purpose of std::in_place_type
. Here is a demonstration.
Does anybody know how to do this better, i.e. without invoking any copy/move constructors?
You have extra copies with std::tuple
usage instead of std::forward_as_tuple
.
In addition, you have extra copy with "recursive" std::tuple_cat
.
For the later, you might concatenate factories or types with conversion operator:
template <typename F>
struct ConversionFactory{
operator std::invoke_result_t<F>() { return f(); }
F f;
};
template <typename... Types>
struct Foo
{
std::tuple<Types...> data;
Foo(const Foo&) = default;
static auto m_init() { return std::tuple{}; }
template <typename T, typename... Ts>
static auto m_init(const std::in_place_type_t<T>&, Ts&&... args) {
using indices = Indexer<0, std::in_place_type_t<T>, Ts...>::next;
auto lambda1 = [&args...]<std::size_t... Is>(std::index_sequence<Is...>)
{
return std::tuple(ConversionFactory([&](){ return T{std::get<Is>(std::forward_as_tuple(std::forward<Ts>(args)...))...}; }));
};
auto lambda2 = [&args...]<std::size_t... Is>(std::index_sequence<Is...>) {
return m_init(std::get<Is>(std::forward_as_tuple(std::forward<Ts>(args)...))...);
};
return std::tuple_cat(lambda1(typename indices::arg_seq{}), lambda2(typename indices::rest_seq{}));
}
template <typename... Ts>
explicit Foo(Ts&&... ts) : data(m_init(std::forward<Ts>(ts)...)) {}
template <typename... Ts>
requires (std::is_same_v<Ts, Types> && ...)
explicit Foo(Ts&&... ts) : data(std::forward<Ts>(ts)...) {}
};