How does std::apply forward parameters without explicit std::forward?
Asked Answered
I

2

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?

Incorrigible answered 21/12, 2016 at 10:34 Comment(0)
C
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>&.

Clynes answered 21/12, 2016 at 10:40 Comment(10)
what if we pass to the get std::tuple<int&&>& and then after get we would perfect forward result element? would it work the same?Incorrigible
you cannot create a std::tuple of reference? so what is std::forward_as_tuple doing?Incorrigible
Of course you can create a tuple of references ideone.com/rm8rAZStiver
@Clynes 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...Incorrigible
@Incorrigible reference collapsing kicks in, the value category of tuple<int&&>& is lvalue, hence you get tuple_element_t<0, std::tuple<int&&>>& which is int&& & -> int&Cable
@PiotrSkotnicki tuple_element_t<0, std::tuple<int&&>> is int&& right? So if we forward<tuple_element_t<0, decay_t<decltype(tup_ref)>>>(get<0>(tup_ref)) we would end up with forwarded rvalue reference on int, or not?Incorrigible
you end up with an rvalue expression referring to that int memberCable
you never get a copy with std::get, what happens next depends on how the expression is usedCable
@Incorrigible 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" partCable
@PiotrSkotnicki After your comments I understand that my answer is fuzzy, so I will try update it as soon as I can.Clynes
S
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.

Stiver answered 21/12, 2016 at 10:37 Comment(6)
exactly that's my point we might want to pass rvalue references to the function fIncorrigible
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?Incorrigible
@Incorrigible - 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.Stiver
@Incorrigible - In case my previous example wasn't to well written, an illustration: coliru.stacked-crooked.com/a/9bcc65c3929968cfStiver
Yup when using references we need to be careful... but it might be really time-saving...Incorrigible
@Incorrigible - 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 :)Stiver

© 2022 - 2024 — McMap. All rights reserved.