12

I want to implement a generic tuple_map function that takes a functor and an std::tuple, applies the functor to every element of this tuple and returns an std::tuple of results. The implementation is pretty straightforward, however the question arises: what type should this function return? My implementation used std::make_tuple. However, here std::forward_as_tuple was suggested.

To be more specific, the implementation (handling of empty tuples is omitted for brevity):

#include <cstddef>
#include <tuple>
#include <type_traits>
#include <utility>

template<class Fn, class Tuple, std::size_t... indices>
constexpr auto tuple_map_v(Fn fn, Tuple&& tuple, std::index_sequence<indices...>)
{
    return std::make_tuple(fn(std::get<indices>(std::forward<Tuple>(tuple)))...);
    //          ^^^
}

template<class Fn, class Tuple, std::size_t... indices>
constexpr auto tuple_map_r(Fn fn, Tuple&& tuple, std::index_sequence<indices...>)
{
    return std::forward_as_tuple(fn(std::get<indices>(std::forward<Tuple>(tuple)))...);
    //          ^^^
}

template<class Tuple, class Fn>
constexpr auto tuple_map_v(Fn fn, Tuple&& tuple)
{ 
    return tuple_map_v(fn, std::forward<Tuple>(tuple), 
        std::make_index_sequence<std::tuple_size_v<std::remove_reference_t<Tuple>>>{});
}

template<class Tuple, class Fn>
constexpr auto tuple_map_r(Fn fn, Tuple&& tuple)
{ 
    return tuple_map_r(fn, std::forward<Tuple>(tuple), 
        std::make_index_sequence<std::tuple_size_v<std::remove_reference_t<Tuple>>>{});
}

In the case 1 we use std::make_tuple which decays type of each argument (_v for value), and in the case 2 we use std::forward_as_tuple which preserves references (_r for reference). Both cases have their pros and cons.

  1. Dangling references.

    auto copy = [](auto x) { return x; };
    auto const_id = [](const auto& x) -> decltype(auto) { return x; };
    
    auto r1 = tuple_map_v(copy, std::make_tuple(1));
    // OK, type of r1 is std::tuple<int>
    
    auto r2 = tuple_map_r(copy, std::make_tuple(1));
    // UB, type of r2 is std::tuple<int&&>
    
    std::tuple<int> r3 = tuple_map_r(copy, std::make_tuple(1));
    // Still UB
    
    std::tuple<int> r4 = tuple_map_r(const_id, std::make_tuple(1));
    // OK now
    
  2. Tuple of references.

    auto id = [](auto& x) -> decltype(auto) { return x; };
    
    int a = 0, b = 0;
    auto r1 = tuple_map_v(id, std::forward_as_tuple(a, b));
    // Type of r1 is std::tuple<int, int>
    ++std::get<0>(r1);
    // Increments a copy, a is still zero
    
    auto r2 = tuple_map_r(id, std::forward_as_tuple(a, b));
    // Type of r2 is std::tuple<int&, int&>
    ++std::get<0>(r2);
    // OK, now a = 1
    
  3. Move-only types.

    NonCopyable nc;
    auto r1 = tuple_map_v(id, std::forward_as_tuple(nc));
    // Does not compile without a copy constructor 
    
    auto r2 = tuple_map_r(id, std::forward_as_tuple(nc));
    // OK, type of r2 is std::tuple<NonCopyable&>
    
  4. References with std::make_tuple.

    auto id_ref = [](auto& x) { return std::reference_wrapper(x); };
    
    NonCopyable nc;
    auto r1 = tuple_map_v(id_ref, std::forward_as_tuple(nc));
    // OK now, type of r1 is std::tuple<NonCopyable&>
    
    auto r2 = tuple_map_v(id_ref, std::forward_as_tuple(a, b));
    // OK, type of r2 is std::tuple<int&, int&>
    

(Probably, I got something wrong or missed something important.)

It seems that make_tuple is the way to go: it doesn't produce dangling references and still can be forced to deduce a reference type. How would you implement tuple_map (and what would be the pitfalls associated with it)?

Evg
  • 25,259
  • 5
  • 41
  • 83
  • Interesting question. But shouldn't it be the responsibility of the person writing the call to make sure the behavior is well defined? For instance, if we take `auto fn = [](auto const& x) { return std::cref(x); }; auto a = fn(2);` We would get a dangling reference, without any tuple involved. My conclusion would be to go with `forward_as_tuple` knowing that (little) caveat – Rerito Aug 07 '18 at 13:36
  • Did You think of just making separate overloads/SFINAE versions for lvalues and rvalues? I think this would allow You to get the best of both worlds – bartop Aug 07 '18 at 13:39

1 Answers1

6

The problem you highlighted in your question is that using std::forward_as_tuple on a functor that returns by value will leave you with an rvalue reference in the resulting tuple.

By using make_tuple you cannot keep lvalue-refs, however by using forward_as_tuple, you cannot keep plain values. You can instead rely on std::invoke_result to find out what are the types your result tuple must hold and use the appropriate std::tuple constructor.

template<class Fn, class Tuple, std::size_t... indices>
constexpr auto tuple_map_r(Fn fn, Tuple&& tuple, std::index_sequence<indices...>) {
    using tuple_type = std::tuple<
        typename std::invoke_result<
            Fn, decltype(std::get<indices>(std::forward<Tuple>(tuple)))
        >::type...
    >;
    return tuple_type(fn(std::get<indices>(std::forward<Tuple>(tuple)))...);
}

This way you preserve the value category of the result of the fn call. Live demo on Coliru

Rerito
  • 5,886
  • 21
  • 47
  • Am I correct that with template argument deduction we don't need `tuple_type` and can simply `return std::tuple(fn(...)...)`? I even wanted to include this alternative into my question, but (erroneously) thought it was equivalent to `forward_as_tuple`. – Evg Aug 07 '18 at 14:26
  • @Evgeny Correct. With template deduction guides it would work I think (I am not too confident since I am not familiar with them yet though...). – Rerito Aug 07 '18 at 14:28
  • 1
    @Evgeny after some [testing](http://coliru.stacked-crooked.com/a/a385ca7c5750e0db), it appears that the deduction guides for `std::tuple` decay their arguments... So explicit type is still the only way to go it seems! – Rerito Aug 07 '18 at 14:33
  • 1
    I've just made a similar test and was going to write a comment that `std::tuple(...)` doesn't really work. :) – Evg Aug 07 '18 at 14:38
  • 4
    @Evgeny `tuple(x...)` is equivalent to `make_tuple(x...)`, with the exception that `reference_wrapper` stays as `reference_wrapper` and doesn't become `T&`. – Barry Aug 07 '18 at 15:07