Search code examples
c++tuplesc++20metaprogramming

How to construct a tuple with a superset of types from tuples with non-overlapping subset of types


Suppose I have a tuple-like that is essentially a wrapper around std::tuple (so a solution for std::tuple works too). How would I go writing a constructor/tuple_cat equivalent that can take an arbitrary amount of arguments of the same templated type but with each having a subset of the template arguments of the return.

The problem with just using std::tuple_cat is that the return type is based on arguments. What I would like to do is have a fixed return type, so that any member not passed in inside a tuple would be default-constructed. It also allows for duplicate types while I would like to error or any overlap in type arguments to the arguments. Ordering is preserved too

An example of what I mean, the constraints in comments are enforced by requires clauses

template<typename... Ts>
// Ts constrained to only one of each type
struct MyTuple {
    template<typename... Tuples>
    // Tuples constrained to all be instantiations of MyTuple
    // each Tuples<Us...> constrained that Us is strictly a subset of types in Ts
    // every pair of Tuples<Us...> constrained to not have any overlap in their Us
    // const& used for simplicity to not get into std::forward-ing everything
    MyTuple(Tuples const&... args)
    {
        // no clue
        // using initializer list would be better, but even less of a clue on that
    }

    std::tuple<Ts...> mTuple;
}

Usage would be something like:

// A..E are default constructible, distinct types
using SuperTuple = MyTuple<A, B, C, D, E>;
// A, B, C and E are passed in as arguments, D is default-constructed
auto super = SuperTuple{MyTuple<A>{}, MyTuple<E,B>{}, MyTuple<C>{}};

Is this even possible?


Solution

  • Here's an implementation that

    1. Doesn't default construct unnecessary elements
    2. Forwards correctly

    The difficulty in your requirements lie in how the std::tuple constructor must have the correct arity. To remedy that, we create an "identity" type default_construct_t that does the right thing when we're missing an argument, similar to how 0 is the identity for sums.

    To avoid recursion or a large amount of metaprogramming trying to index into the pack of tuples, we conveniently reuse default_construct_t to fold across types in std::common_type_t.

    #include<tuple>
    #include<utility>
    
    struct default_construct_t
    {
        using type = default_construct_t;
    
        template<typename T>
        operator T() { return T(); }
    };
    
    template<typename, typename>
    struct has_element : std::false_type
    {
    };
    
    template<typename... Args, typename T>
    requires ((std::is_same_v<T, Args> || ...))
    struct has_element<std::tuple<Args...>, T>
        : std::true_type
    {
    };
    
    template<typename Tup, typename T>
    concept contains = has_element<std::decay_t<Tup>, T>::value;
    
    template<typename T, typename... Tuples>
    decltype(auto) find(Tuples&&... tuples)
    {
        using R = std::common_type_t<
            std::conditional_t<
                contains<decltype(tuples), T>,
                std::type_identity<decltype(tuples)>,
                default_construct_t
            >...
        >::type;
    
        if constexpr(std::is_same_v<R, default_construct_t>)
            return default_construct_t{};
        else
            return std::get<T>(std::get<R>(
                std::forward_as_tuple(std::forward<Tuples>(tuples)...)
            ));
    }
    
    template<typename... Ts>
    struct MyTuple
    {
        template<typename... Tuples>
        MyTuple(Tuples&&... tuples)
            : mTuple{find<Ts>(std::forward<Tuples>(tuples)...)...}
        {
        }
    
        std::tuple<Ts...> mTuple;
    };
    

    Live.