std::forward()

In this note, we simulate part of std's type_trait to understand the magic part of C++.

std::is_lvalue_reference && std::is_rvalue_reference

template <typename T>
struct jr_is_lvalue_reference       : public std::false_type { }

template <typename T>
struct jr_is_lvalue_reference<T&>   : public std::true_type { }

template <typename T>
struct jr_is_rvalue_reference       : public std::false_type { }

template <typename T>
struct jr_is_rvalue_reference<T&&>  : public std::true_type { }

Usage

template <typename T>
void foo(){
    cout<<std::jr_is_lvalue_reference<T>::value<<endl;
    cout<<std::jr_is_rvalue_reference<T>::value<<endl;
    cout<<endl;
}

int main(int argc, const char* argv[]){
    std::vector<int> v;
    std::vector<int>& ref = v;
    
    // call foo with explicit instantiation
    foo<decltype(ref)>();
    foo<decltype(std::move(v))>();
}

The output is

1
0

0
1

Let's try to understand is, first, is the reference collapsing rule of C++.

class X { };

X& &&, X&& &, X& &  -->  X&
X&& &&              -->  X&&

When we call

foo<decltype(ref)>();
/*  
 * T of foo() is std::vector<int>&, 
 *  std::jr_is_lvalue_reference<T>  =>  std::jr_is_lvalue_reference<std::vector<int>&>
 *  it calls the partial instantiation:
 *
 *      template <typename T>
 *      struct jr_is_lvalue_reference<T&> : public true_type{ }
 *
 *  here T is std::vector<int>
 *
 */
 
 foo<decltype(std::move(v))>();
/* 
 * Almost same,
 * Here just call foo() with rvalue reference instantiation
 */

std::remove_reference

template <typename T>
struct jr_remove_reference     { typedef type T; }

template <typename T>
struct jr_remove_reference<T&> { typedef type T; }

template <typename T>
struct jr_remove_reference<T&&>{ typedef type T; }

Just a struct and 2 partial instantiations.

std::forward()

And now let's simulate std::forward()std::forward().

template <typename T>
T&& jr_forward(typename jr_remove_reference<T>::type& t){
    return static_cast<T&&>(t);
}

templat <typename T>
T&& jr_forwad(typename jr_remove_reference<T>::type&& t){
    static_assert(!jr_is_lvalue_reference<T>::value, "cannot forward a rvalue as a lvalue");
    return static_cast<T&&>(t);
}

Since we try to forward a type to another type, we must say which type we want to forward to, it's obvious that we must call std::forward()std::forward() or jr_forward()jr\_forward() with explicit instantiation.

Use and understand std::forward()

Let's try to understand this.

class X{ };

void g(X&){
    cout<<"f() for X&"<<endl;
}

void g(X&&){
    cout<<"f() for X&&"<<endl;
}

void g(const X&){
    cout<<"f() for const X&"<<endl;
}

int main(int argc, const char* argv[]){
    X v;
    const X c;
    
    g(v);
    g(c);
    g(X{});
    g(std::move(v));
}

Output is

g() for X& 
g() for const X& 
g() for X&& 
g() for X&& 

If we want to unify all variations of g()g() in a f()f(), we may write something like this.


template <typename T>
void f(T& val){
    g(std::forward<T>(val));
}

And in main()main()

int main(){
    X v;
    const X c;
    
    f(v);
    f(c);
}

The output is

g() for X&& 
g() for const X& 

Is there some thing wrong.

For f(v)f(v), which should be called isg(X&)g(X\&), it called g(X&&)g(X\&\&) is called, but it did the right thing in f(c)f(c).

Sure, when you call f(v)f(v), TT for f()f() is XX,actually, we called f<X&>(v)f<X\&>(v) and forward<X>forward<X> returns X&&X\&\&.

but in f(c)f(c), constconst won't be droped during function calling, so, let suppose

using type = const X&.

We're actually calling f<type&>f<type\&> in f(c)f(c). TT of f()f() is const  X&const~~X\&, and forward<constX&>forward<const X\&> returns const  X&  &&const~~X\&~~\&\&, according to the collapsing rule, constX&  &&const X\&~~\&\& becomes const X&const~X\&.

Beside this, there're more errors. if we do this in main()main()

int main(){
    X v;
    const X c;
    
    f(X());
    f(std::move(v));
}

It won't even compile, the error is No  matching  function  for  call  to  fNo ~~ matching ~~ function ~~ for ~~ call ~~ to ~~ 'f'

Sure, f()f() takes a lvalue reference as parameter, how can we pass a rvalue reference to it !!!

Perfect forwarding

And to uniform all g()g()s, we can write something like this, it's the trikey use of rvalue reference.

template <typename T>
void f(T&& val){
    g(std::forward<T>(val));
}

int main(int argc, const char* argv[]){
    X v;
    const X c;
    
    f(v);
    f(c);
    f(X{});
    f(std::move(v));
}

The output is

g() for X& 
g() for const X& 
g() for X&& 
g() for X&& 

It works well.

And, why? First, when we callf(v)f(v) . We actually called f<X&>(v)f<X\&>(v) , the TT for f()f() is X&X\& , std::forward<X&>std::forward<X\&> returns X& &&X\&~\&\& , a rvalue reference to a lvalue reference, it collapsed as X&X\& according the C++ reference collapsing rule, so std::forward<X&>std::forward<X\&> actually returns X&X\& , it's straight forward that it calls g(X&)g(X\&) .

And when we call f(c)f(c). We actually called f<constX&>(v)f<const X\&>(v). And you're encouraged to do the rest deduction by yourself.

And last, when we call f(X)f(X{}) for f(std::move(v))f(std::move(v)), we call f()f() with a rvalue reference, we just called f<X>(val)f<X>(val), the TT for f()f() is just XX.

Summarize

Let'e review the perfect forwarding.

template <typename T>
void f(T&& val){
    g(std::forward<T>(val));
}

To summarize, what we're actually is that, std::forward()std::forward() always return a rvalue reference, when we pass a rvalue, we get a straight-forward rvalue, when we pass a lvalue, according to C++ reference collapsing rule, we get a lvalue.