Search code examples
c++variadic-templates

Transform tuple type to another tuple type


Suppose I've got a tuple type std::tuple<x,y,z>, or maybe std::tuple<a,b>. I'd like a general purpose way to transform the types in my tuple, e.g. "functionize" to get

std::tuple<std::function<void(x)>,
           std::function<void(y)>,
           std::function<void(z)>>

or maybe get storage for shared_pointers like

std::tuple<std::shared_pointer<a>,
           std::shared_pointer<b>>

How would I achieve this with C++17? C++20 answers interesting but not applicable in my current situation.

Motivation: I've got a class that's going to be parameterized by a list of arbitrary and not necessarily unique types, and would like to have a std::function member for each type in the list.


Solution

  • Template specialization is probably the easiest way.

    template <typename T>
    struct functionize;
    
    template <typename... Ts>
    struct functionize<std::tuple<Ts...>> {
        using type = std::tuple<std::function<void(Ts)>...>;
    }
    
    using MyTuple = std::tuple<int, double, char>;
    using MyTupleFunctionized = typename functionize<MyTuple>::type;
    

    To make it more generic, you can accept a template template parameter to apply to the pack.

    template <typename Tuple, template <typename> typename Component>
    struct transform_tuple;
    
    template <typename... Ts, template <typename> typename Component>
    struct transform_tuple<std::tuple<Ts...>, Component> {
        using type = std::tuple<Component<Ts>...>;
    }
    
    using MyTuple = std::tuple<int, double, char>;
    using MyTransformedTuple = typename transform_tuple<MyTuple, std::shared_ptr>::type;
    

    The more general solution works best with c++17 or later. Prior to that it would only match on a template with exactly 1 parameter. After c++17 it would also be able to match on something like std::vector which has 2 template parameters where the second one has a default argument.

    Edit:
    As pointed out in the comments by @Jarod42 and @Caleth we can improve on this a bit more.

    template <typename Tuple, template <typename...> typename Component>
    struct transform_tuple;
    

    A parameter pack for the template template parameter allows us to pass something like a std::vector in from c++11 and forward. That only leaves issues if we want to pass something that mixes type and non-type parameters, like std::array.

    We can partially solve that issue by using template aliases.

    template <typename T>
    using FixedArray10 = std::array<T, 10>;
    
    using MyTransformedTuple = typename transform_tuple<MyTuple, FixedArray10>::type;