Search code examples
c++templatestuplesc++20type-traits

Apply multiple tuples at the same time in C++20


I have N tuples Foo, Bar, ..., Baz with 'M' arbitrary element types. I can run std::apply on each tuple, and invoke some arbitrary operation on its elements:

#include <tuple>
#include <type_traits>

auto operation(auto& foo, auto& bar)
{

//operation can be arbitrary

    return foo + bar;
}


int main()
{
    std::tuple<int, float, double> Foo{1, 2.0f, 3.0};
    std::tuple<float, bool, char> Bar{1.0f, true, 'a'};

    auto result = std::apply(
        [&](auto &&...foo) {
            return std::apply([&](auto &&...bar) { 

return std::make_tuple(operation(foo, bar)...); }, Bar);
        },
        Foo);



    return 0;
}

These expressions become verbose and ugly when the number of tuples increase. How would I proceed to design a multi_apply:

    auto result = multi_apply([&](auto&& ... foo, auto&& ... bar, ..., auto&& ... baz)
    {
        return std::make_tuple(operation(foo, bar, ..., baz)...);
    }, Foo, Bar, ..., Baz);

Here is a "2D-variadic" example, demonstrating what multi_apply should handle.

#include <tuple>
#include <type_traits>

auto operation(auto &...foo_bar_baz_elem)
{

    //operation can be arbitrary

    return (foo_bar_baz_elem + ...);
}


int main()
{
    std::tuple<int, float, double> Foo{1, 2.0f, 3.0};
    std::tuple<float, bool, char> Bar{1.0f, true, 'a'};

    //...

    std::tuple<char, float, char> Baz{1.0f, true, 'a'};

    auto result = std::apply(
        [&](auto &&...foo) {
            return std::apply(
                [&](auto &&...bar) {
                    // apply ...
                    //...
                    return std::apply([&](auto &&...baz) { return std::make_tuple(operation(foo, bar, ..., baz)...); },
                                      Bar);
                },
                Baz);
        },
        Foo);


    return 0;
}

Note: Input argument size of operation() is known at the time of Foo, Bar,..., Baz declaration, but multi_apply needs to be able to handle all sizes.


Solution

  • This lets you pass around compile time natural numbers as values:

    template<std::size_t I>
    using index_t = std::integral_constant<std::size_t, I>;
    

    And this is a tuple of them:

    template<std::size_t...Is>
    using indexes_t = std::tuple< index_t<Is>... >;
    

    Next, extract the indexes of a tuple:

    template<std::size_t...Is>
    constexpr indexes_t<Is...> to_indexes( std::index_sequence<Is...> )
    { return {}; }
    
    template<class...Ts>
    constexpr auto to_indexes( std::tuple<Ts...> const& ) {
      return to_indexes( std::make_index_sequence<sizeof...(Ts)>{} );
    }
    

    Also, a nice primitive "map" - this applies f to each element of a tuple and makes a tuple from the result.

    template<class Tuple, class F>
    constexpr auto map_tuple( Tuple&& tuple, F f ) {
      return std::apply(
        [&](auto&&...ts){
          return std::make_tuple( f(ts)... );
        },
        std::forward<Tuple>(tuple)
      );
    }
    

    we can now define shuffle_tuples. The empty shuffle is empty:

    constexpr std::tuple<> shuffle_tuples() { return {}; }
    

    For non-empty shuffles, we take the number of elements in the first tuple and then pick those in turn for each of the tuples:

    template<class T0, class...Tuples>
    constexpr auto shuffle_tuples( T0&& t0, Tuples&&... tuples ) {
      return map_tuple(
        to_indexes(t0),
        [&](auto I) {
          return std::make_tuple(
            std::get<I>(std::forward<T0>(t0),
            std::get<I>(std::forward<Tuples>(tuples)...
          );
        }
      );
    }
    

    and that should do it.