10

Consider possible implementation of std::apply:

namespace detail {
template <class F, class Tuple, std::size_t... I>
constexpr decltype(auto) apply_impl(F &&f, Tuple &&t, std::index_sequence<I...>) 
{
    return std::invoke(std::forward<F>(f), std::get<I>(std::forward<Tuple>(t))...);
}
}  // namespace detail

template <class F, class Tuple>
constexpr decltype(auto) apply(F &&f, Tuple &&t) 
{
    return detail::apply_impl(
        std::forward<F>(f), std::forward<Tuple>(t),
        std::make_index_sequence<std::tuple_size_v<std::decay_t<Tuple>>>{});
}

Why when invoking the function(f) with tuple of parameters to pass(t) we don't need to perform std::forward on each element of the tuple std::get<I>(std::forward<Tuple>(t))... in the implementation?

SingerOfTheFall
  • 29,228
  • 8
  • 68
  • 105
W.F.
  • 13,888
  • 2
  • 34
  • 81

2 Answers2

8

You do not need to std::forward each element because std::get is overloaded for rvalue-reference and lvalue-reference of tuple.

std::forward<Tuple>(t) will give you either a lvalue (Tuple &) or an rvalue (Tuple &&), and depending on what you get, std::get will give you a T & (lvalue) or a T && (rvalue). See the various overload of std::get.


A bit of details about std::tuple and std::get -

As mentioned by StoryTeller, every member of a tuple is an lvalue, whether it has been constructed from an rvalue or a lvalue is of no relevance here:

double a{0.0};
auto t1 = std::make_tuple(int(), a);
auto t2 = std::make_tuple(int(), double());

The question is - Is the tuple an rvalue? If yes, you can move its member, if no, you have to do a copy, but std::get already take care of that by returning member with corresponding category.

decltype(auto) a1 = std::get<0>(t1);
decltype(auto) a2 = std::get<0>(std::move(t1));

static_assert(std::is_same<decltype(a1), int&>{}, "");
static_assert(std::is_same<decltype(a2), int&&>{}, "");

Back to a concrete example with std::forward:

template <typename Tuple>
void f(Tuple &&tuple) { // tuple is a forwarding reference
    decltype(auto) a = std::get<0>(std::forward<Tuple>(tuple));
}

f(std::make_tuple(int())); // Call f<std::tuple<int>>(std::tuple<int>&&);
std::tuple<int> t1;
f(t1); // Call f<std::tuple<int>&>(std::tuple<int>&);

In the first call of f, the type of a will be int&& because tuple will be forwarded as a std::tuple<int>&&, while in the second case its type will be int& because tuple will be forwarded as a std::tuple<int>&.

Community
  • 1
  • 1
Holt
  • 36,600
  • 7
  • 92
  • 139
  • what if we pass to the `get` `std::tuple&` and then after `get` we would perfect forward result element? would it work the same? – W.F. Dec 21 '16 at 10:58
  • 2
    `you cannot create a std::tuple of reference`? so what is `std::forward_as_tuple` doing? – W.F. Dec 21 '16 at 11:01
  • Of course you can create a tuple of references http://ideone.com/rm8rAZ – StoryTeller - Unslander Monica Dec 21 '16 at 11:01
  • @Holt in my understanding when you create a field to store that rvalue reference it would loose its rvalue reference properties as it would have a name... and so you to achieve perfect forwarding you would need to `std::forward` it, again? But of course I might be missing something... – W.F. Dec 21 '16 at 11:06
  • @W.F. reference collapsing kicks in, the value category of `tuple&` is *lvalue*, hence you get `tuple_element_t<0, std::tuple>&` which is `int&& & -> int&` – Piotr Skotnicki Dec 21 '16 at 11:09
  • @PiotrSkotnicki `tuple_element_t<0, std::tuple>` is `int&&` right? So if we `forward>>(get<0>(tup_ref))` we would end up with forwarded rvalue reference on int, or not? – W.F. Dec 21 '16 at 11:13
  • you end up with an rvalue expression referring to that `int` member – Piotr Skotnicki Dec 21 '16 at 11:17
  • 2
    you never get a copy with `std::get`, what happens next depends on how the expression is used – Piotr Skotnicki Dec 21 '16 at 11:21
  • 5
    @W.F. probably you should change your mindset a bit and think of `std::forward` as a conditional `std::move`, and of `std::move` as a plain cast, nothing more. the above answer is, at least for me, hard to parse, especially the *"You cannot forward something that is not a forwarding reference"* part – Piotr Skotnicki Dec 21 '16 at 11:53
  • @PiotrSkotnicki After your comments I understand that my answer is fuzzy, so I will try update it as soon as I can. – Holt Dec 21 '16 at 12:03
3

std::forward is used to make sure that everything arrives at the call site with the correct value category.

But every member of a tuple is an lvalue, even if it's a tuple of rvalue references.

StoryTeller - Unslander Monica
  • 165,132
  • 21
  • 377
  • 458
  • exactly that's my point we might want to pass rvalue references to the function `f` – W.F. Dec 21 '16 at 10:38
  • even if the forward would be done inside the std::invoke the function wouldn't be able to know if the parameter is rvalue reference, no? – W.F. Dec 21 '16 at 10:40
  • @W.F. - We would be able to know. But like Holt elaborated, the only time we can assume that the temporary bound to the rvalue reference in the tuple is still a valid object, is if the tuple itself is bound to an rvalue reference. So it is the value category of the tuple itself that should win out always. – StoryTeller - Unslander Monica Dec 21 '16 at 11:07
  • 1
    @W.F. - In case my previous example wasn't to well written, an illustration: http://coliru.stacked-crooked.com/a/9bcc65c3929968cf – StoryTeller - Unslander Monica Dec 21 '16 at 11:19
  • 1
    Yup when using references we need to be careful... but it might be really time-saving... – W.F. Dec 21 '16 at 11:24
  • @W.F. - I think the standard, as always, leaves such considerations in the hands of the developer. It handles the safe case by itself, and if you are careful and know what you're doing, you can just `std::move` the tuple :) – StoryTeller - Unslander Monica Dec 21 '16 at 11:26