Search code examples
c++17variadic-templatesstdtupleparameter-packfold-expression

How to defer expanding a parameter pack?


I was toying around with tuples. I wanted to zip an arbitrary number of tuples. There is probably a better way to do that than what I came up with, but my solution led me to a problem that is interesting in itself.

Sometimes, you want to expand one parameter pack at a time, and those parameter packs can be template arguments and function arguments in the same call. I could not find an obvious way to expand Is without expanding ts, besides stuffing ts into a std::tuple and unpacking it again using std::get<I>.

Obviously, it's preferable not to split one function into five functions. I know I could use lambdas, but I'm not sure that would be any cleaner.

Is there a nice way to defer expanding ts?

https://godbolt.org/z/sY5xMTa7P

#include <iostream>
#include <tuple>
#include <string_view>

template <typename T, typename... Ts>
auto get_first(T t, Ts...) {
    return t;
}

template <size_t I, typename... Ts>
auto zip2_impl4(Ts... ts) {
    return std::make_tuple(std::get<I>(ts)...);
}

template <size_t I, typename T, size_t... Is>
auto zip2_impl3(T t, std::index_sequence<Is...>) {
    return zip2_impl4<I>(std::get<Is>(t)...);
}

template <size_t I, typename T>
auto zip2_impl2(T t) {
    using size = std::tuple_size<T>;
    using seq = std::make_index_sequence<size::value>;
    return zip2_impl3<I>(t, seq{});
}

template <size_t... Is, typename... Ts>
auto zip2_impl(std::index_sequence<Is...> seq, Ts... ts) {
    // need to defer expanding the pack ts,
    // because the packs Is and ts need to expand separately
    auto t = std::make_tuple(ts...);
    return std::make_tuple(zip2_impl2<Is>(t)...);
}

template <typename... Ts>
auto zip2(Ts... ts) {
    using size = std::tuple_size<decltype(get_first(ts...))>;
    using seq = std::make_index_sequence<size::value>;
    return zip2_impl(seq{}, ts...);
}

int main() {
    using namespace std::literals;
    auto ints = std::make_tuple(1,2,3);
    auto svs1 = std::make_tuple("a"sv, "b"sv, "c"sv);
    auto svs2 = std::make_tuple("d"sv, "e"sv, "f"sv);
    auto zipped = zip2(ints, svs1, svs2);

    std::apply([](auto... args) {
        (std::apply([](auto... args) {
            ((std::cout << args), ...);
        }, args), ...);
    }, zipped);

    return 0;
}
output: 1ad2be3cf

Solution

  • If I understood you correctly: https://godbolt.org/z/e4fvr45rz

    #include <type_traits>
    #include <tuple>
    #include <cstddef>
    
    template <size_t Indx,typename ... Tuples>
    constexpr auto zip(Tuples ... tuples)
    {
        return std::make_tuple(std::get<Indx>(tuples)...);
    }
    
    template <typename ... Tuples, size_t ... Indx>
    constexpr auto zip(std::index_sequence<Indx...>, Tuples... tuples)
    {
        // will expand simultaneously, not what you want...
        // return std::make_tuple(std::make_tuple(std::get<Indx>(tuples)...));
        return std::make_tuple(zip<Indx>(tuples...)...);
    }
    
    template <typename ... Tuples>
    constexpr auto zip(Tuples ... tuples)
    {
        return zip(std::make_index_sequence<sizeof...(Tuples)>(), tuples...);
    }
    
    #include <iostream>
    #include <string_view>
    
    int main()
    {
        using namespace std::literals;
        auto ints = std::make_tuple(1,2,3);
        auto svs1 = std::make_tuple("a"sv, "b"sv, "c"sv);
        auto svs2 = std::make_tuple("d"sv, "e"sv, "f"sv);
        auto zipped = zip(ints, svs1, svs2);
    
        std::apply([](auto... args) {
            (std::apply([](auto... args) {
                ((std::cout << args), ...);
                std::cout << std::endl;
            }, args), ...);
        }, zipped);
    
        return 0;
    }
    

    Output:

    Program returned: 0
    1ad
    2be
    3cf
    

    So there is a way, I guess. You have to have your parameter packs in different contexts: one in the function arguments, another in the template argumetns. If you try to expand both packs in one "context" you will get a compile-time error:

    // this will not compile
    // return std::make_tuple(std::make_tuple(std::get<Indx>(tuples)...)...);
    
    // and this will expand them simultaneously, giving you "1bf" as a result
    return std::make_tuple(std::make_tuple(std::get<Indx>(tuples)...));
    

    UPDATE: https://godbolt.org/z/9E1zj5q4G - more generic solution:

    template <typename FirstTuple, typename ... Tuples>
    constexpr auto zip(FirstTuple firstTuple, Tuples ... tuples)
    {
        return zip(std::make_index_sequence<std::tuple_size_v<FirstTuple>>(), firstTuple, tuples...);
    }
    
    int main()
    {
        using namespace std::literals;
        auto ints = std::make_tuple(1,2,3);
        auto svs1 = std::make_tuple("a"sv, "b"sv, "c"sv);
        auto svs2 = std::make_tuple("d"sv, "e"sv, "f"sv);    
        auto svs3 = std::make_tuple("g"sv, "h"sv, "i"sv);
    
        auto zipped = zip(ints, svs1, svs2, svs3);
    
        std::apply([](auto... args) {
            (std::apply([](auto... args) {
                ((std::cout << args), ...);
                std::cout << std::endl;
            }, args), ...);
        }, zipped);
    
        return 0;
    }
    
    
    Output:
    1adg
    2beh
    3cfi
    

    Instead of number of tuples you pass a number of elements. You have to be sure that each tuple has at least as many elements as the first one. Extra elements will be truncated. If there are too few elements, there will be a compilation error from std::get<>.