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?
The std::forward function as the std::move function aims at implementing move semantics in C++. The function takes a forwarding reference. According to the T template parameter, std::forward identifies whether an lvalue or an rvalue reference has been passed to it and returns a corresponding kind of reference.
std::forward has a single use case: to cast a templated function parameter (inside the function) to the value category (lvalue or rvalue) the caller used to pass it. This allows rvalue arguments to be passed on as rvalues, and lvalues to be passed on as lvalues, a scheme called “perfect forwarding.”
std::move takes an object and casts it as an rvalue reference, which indicates that resources can be "stolen" from this object. std::forward has a single use-case: to cast a templated function parameter of type forwarding reference ( T&& ) to the value category ( lvalue or rvalue ) the caller used to pass it.
What is Perfect Forwarding. Perfect forwarding allows a template function that accepts a set of arguments to forward these arguments to another function whilst retaining the lvalue or rvalue nature of the original function arguments.
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>&
.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With