How does std::apply forward parameters without explicit std::forward?

909 views Asked by At

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?

2

There are 2 answers

10
Holt On BEST ANSWER

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>&.

6
StoryTeller - Unslander Monica On

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.