c++tuplesvariadic-templatestemplate-meta-programmingin-place

How to split variadic template arguments without additional copies?


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?


Solution

  • 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)...) {}
    };
    

    Demo