How Does Std::Forward Work

How does std::forward work?

First, let's take a look at what std::forward does according to the standard:

§20.2.3 [forward] p2

Returns: static_cast<T&&>(t)

(Where T is the explicitly specified template parameter and t is the passed argument.)

Now remember the reference collapsing rules:

TR   R

T& & -> T& // lvalue reference to cv TR -> lvalue reference to T
T& && -> T& // rvalue reference to cv TR -> TR (lvalue reference to T)
T&& & -> T& // lvalue reference to cv TR -> lvalue reference to T
T&& && -> T&& // rvalue reference to cv TR -> TR (rvalue reference to T)

(Shamelessly stolen from this answer.)

And then let's take a look at a class that wants to employ perfect forwarding:

template<class T>
struct some_struct{
T _v;
template<class U>
some_struct(U&& v)
: _v(static_cast<U&&>(v)) {} // perfect forwarding here
// std::forward is just syntactic sugar for this
};

And now an example invocation:

int main(){
some_struct<int> s1(5);
// in ctor: '5' is rvalue (int&&), so 'U' is deduced as 'int', giving 'int&&'
// ctor after deduction: 'some_struct(int&& v)' ('U' == 'int')
// with rvalue reference 'v' bound to rvalue '5'
// now we 'static_cast' 'v' to 'U&&', giving 'static_cast<int&&>(v)'
// this just turns 'v' back into an rvalue
// (named rvalue references, 'v' in this case, are lvalues)
// huzzah, we forwarded an rvalue to the constructor of '_v'!

// attention, real magic happens here
int i = 5;
some_struct<int> s2(i);
// in ctor: 'i' is an lvalue ('int&'), so 'U' is deduced as 'int&', giving 'int& &&'
// applying the reference collapsing rules yields 'int&' (& + && -> &)
// ctor after deduction and collapsing: 'some_struct(int& v)' ('U' == 'int&')
// with lvalue reference 'v' bound to lvalue 'i'
// now we 'static_cast' 'v' to 'U&&', giving 'static_cast<int& &&>(v)'
// after collapsing rules: 'static_cast<int&>(v)'
// this is a no-op, 'v' is already 'int&'
// huzzah, we forwarded an lvalue to the constructor of '_v'!
}

I hope this step-by-step answer helps you and others understand just how std::forward works.

What's the difference between std::move and std::forward

std::move takes an object and allows you to treat it as a temporary (an rvalue). Although it isn't a semantic requirement, typically a function accepting a reference to an rvalue will invalidate it. When you see std::move, it indicates that the value of the object should not be used afterwards, but you can still assign a new value and continue using it.

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

To illustrate:

void overloaded( int const &arg ) { std::cout << "by lvalue\n"; }
void overloaded( int && arg ) { std::cout << "by rvalue\n"; }

template< typename t >
/* "t &&" with "t" being template param is special, and adjusts "t" to be
(for example) "int &" or non-ref "int" so std::forward knows what to do. */
void forwarding( t && arg ) {
std::cout << "via std::forward: ";
overloaded( std::forward< t >( arg ) );
std::cout << "via std::move: ";
overloaded( std::move( arg ) ); // conceptually this would invalidate arg
std::cout << "by simple passing: ";
overloaded( arg );
}

int main() {
std::cout << "initial caller passes rvalue:\n";
forwarding( 5 );
std::cout << "initial caller passes lvalue:\n";
int x = 5;
forwarding( x );
}

As Howard mentions, there are also similarities as both these functions simply cast to reference type. But outside these specific use cases (which cover 99.9% of the usefulness of rvalue reference casts), you should use static_cast directly and write a good explanation of what you're doing.

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

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

Usage of std::forward vs std::move

You cannot use std::forward without explicitly specifying its template argument. It is intentionally used in a non-deduced context.

To understand this, you need to really understand how forwarding references (T&& for a deduced T) work internally, and not wave them away as "it's magic." So let's look at that.

template <class T>
void foo(T &&t)
{
bar(std::forward<T>(t));
}

Let's say we call foo like this:

foo(42);
  • 42 is an rvalue of type int.
  • T is deduced to int.
  • The call to bar therefore uses int as the template argument for std::forward.
  • The return type of std::forward<U> is U && (in this case, that's int &&) so t is forwarded as an rvalue.

Now, let's call foo like this:

int i = 42;
foo(i);
  • i is an lvalue of type int.
  • Because of the special rule for perfect forwarding, when an lvalue of type V is used to deduce T in a parameter of type T &&, V & is used for deduction. Therefore, in our case, T is deduced to be int &.

Therefore, we specify int & as the template argument to std::forward. Its return type will therefore be "int & &&", which collapses to int &. That's an lvalue, so i is forwarded as an lvalue.

Summary

Why this works with templates is when you do std::forward<T>, T is sometimes a reference (when the original is an lvalue) and sometimes not (when the original is an rvalue). std::forward will therefore cast to an lvalue or rvalue reference as appropriate.

You cannot make this work in the non-template version precisely because you'll have only one type available. Not to mention the fact that setImage(Image&& image) would not accept lvalues at all—an lvalue cannot bind to rvalue references.

How does std::forward receive the correct argument?

It does bind to the overload of std::forward taking an lvalue:

template <class T>
constexpr T&& forward(remove_reference_t<T>& t) noexcept;

It binds with T == int. This function is specified to return:

static_cast<T&&>(t)

Because the T in f deduced to int. So this overload casts the lvalue int to xvalue with:

static_cast<int&&>(t)

Thus calling the g(int&&) overload.

In summary, the lvalue overload of std::forward may cast its argument to either lvalue or rvalue, depending upon the type of T that it is called with.

The rvalue overload of std::forward can only cast to rvalue. If you try to call that overload and cast to lvalue, the program is ill-formed (a compile-time error is required).

So overload 1:

template <class T>
constexpr T&& forward(remove_reference_t<T>& t) noexcept;

catches lvalues.

Overload 2:

template <class T> constexpr T&& forward(remove_reference_t<T>&& t) noexcept;

catches rvalues (which is xvalues and prvalues).

Overload 1 can cast its lvalue argument to lvalue or xvalue (the latter which will be interpreted as an rvalue for overload resolution purposes).

Overload 2 can can cast its rvalue argument only to an xvalue (which will be interpreted as an rvalue for overload resolution purposes).

Overload 2 is for the case labeled "B. Should forward an rvalue as an rvalue" in N2951. In a nutshell this case enables:

std::forward<T>(u.get());

where you are unsure if u.get() returns an lvalue or rvalue, but either way if T is not an lvalue reference type, you want to move the returned value. But you don't use std::move because if T is an lvalue reference type, you don't want to move from the return.

I know this sounds a bit contrived. However N2951 went to significant trouble to set up motivating use cases for how std::forward should behave with all combinations of the explicitly supplied template parameter, and the implicitly supplied expression category of the ordinary parameter.

It isn't an easy read, but the rationale for each combination of template and ordinary parameters to std::forward is in N2951. At the time this was controversial on the committee, and not an easy sell.

The final form of std::forward is not exactly what N2951 proposed. However it does pass all six tests presented in N2951.

How do two overloaded std::forward work?

I have found cases in which this function is called:

my_forward<int&>(8); // This calls forward(typename remove_reference<_Ty>::type&& _Arg)
But it won't compile 'cause we try convert rvalue to lvalue

my_forward<int&&>(8); // This calls forward(typename remove_reference<_Ty>::type&& _Arg)
But it compiles successfully

If it may be useful for somebody, the purpose of std::forward is to forward operation to another class or function.

Consider the following example:

template<typename T>
void precompute(T & t)
{
std::cout << "lvalue reference";
}

template<typename T,
std::enable_if_t<!std::is_reference<T>::value, bool> = true>
void precompute(T && t)
{
std::cout << "rvalue reference";
}

template<typename T>
void calculate(T && t)
{
precompute(t); // Every time called precompute(T & t)
}

or like this:

template<typename T>
void precompute(T & t)
{
std::cout << "lvalue reference";
}

template<typename T,
std::enable_if_t<!std::is_reference<T>::value, bool> = true>
void precompute(T && t)
{
std::cout << "rvalue reference";
}

template<typename T>
void calculate(T && t)
{
precompute(std::move(t)); // Every time called precompute(T && t)
}

As you can see we have a problem !! Neither of two examples satisfied us.

In first example every time will be called lvalue function and nothing can be done to call first with rvalue.

In second example every time will be called rvalue function and nothing can be done to call first with lvalue.

The solution is to forward making decision to the called function:

template<typename T>
void precompute(T & t)
{
std::cout << "lvalue reference";
}

template<typename T,
std::enable_if_t<!std::is_reference<T>::value, bool> = true>
void precompute(T && t)
{
std::cout << "rvalue reference";
}

template<typename T>
void calculate(T && t)
{
precompute(std::forward<T>(t)); // Will be called either precompute(T & t) or precompute(T && t) depends on type of t
}

In this case we have to be sure that will be called appropriate version.
That is why we forward (means: "Please, check type of t and if it is rvalue -> call rvalue version of function, and if it is lvalue -> call lvalue version of function") this operation to called function.

std::forward through an example

The whole problem stems from the forwarding references using same symbols as rvalue ones, but not being the same.

Take the following code:

template<typename T>
void f(T&& t)
{
//whatever
}

In this case T&& is a forwarding reference. It is neither T&& in the sense of rvalue-reference (reference to temporary), nor is it T&. Compiler deduces that at compile time. Notice though, it does not have type specified either, it's a template paremeter. Only in case of those parameters the forwarding reference semantics applies (another way of writing it down is a auto&& in lambdas, but the deduction works the same way).
Thus when you call int x= 3; f(x); you're effectively calling f(int&). Calling f(3) calls effectively f(int&&) though.

void g(int&& arg)

arg is and rvalue reference to int. Because the type is specified, it's not a template argument! It's always an rvalue reference.

Thus, for your case

void f(int&& int1, int&& int2){
std::cout << "f called!\n";
}

template <class T>
void wrapper(T&& int1, T&& int2){
f(std::forward<T>(int1), std::forward<T>(int2));
}


int main(){
int int1 = 20;
int int2 = 30;
int &int3 = int1;
wrapper(int1, int2); //call wrapper(int&, int&);
//Then f(std::forward<T>(int1), std::forward<T>(int2));-> f(int&, int&), because they are lvalues! Thus, not found, compilation error!
}

Live demo: https://godbolt.org/z/xjTnjcqj8

C++: does std::invoke need perfect forwarding?

Well, I'd say yes but it depends on the case.

You don't need forwarding in that case:

void bar(int);

int main() {
int i = 0;
invokeTest(bar, i);
}

But, you'll need forwarding for this case:

void foo(std::unique_ptr<int>);

int main() {
auto p = std::make_unique<int>();
invokeTest(foo, p); // Won't work, cannot copy unique_ptr
}

When writing std::invoke(fn, args...), the args... are all lvalues, at which point I would recommend taking parameters by Args const&... to be clearer.

If you forward, then you move the object that were rvalues in your parameters:

template <typename CallableType, typename... Args>
auto invokeTest(CallableType&& fn, Args&&... args)
{
return std::invoke(fn, std::forward<Args>(args)...); // all forward value caterories
}

invokeTest(foo, std::move(p)); // works.

If you use Args&&..., use forwarding.



Related Topics



Leave a reply



Submit