What should tuple_map return?

  • A+
Category:Languages

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)?

 


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

Comment

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: