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 .
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 or
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 in a
, we may write something like this.
template <typename T>
void f(T& val){
g(std::forward<T>(val));
}
And in
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 , which should be called is
, it called
is called, but it did the right thing in
.
Sure, when you call ,
for
is
,actually, we called
and
returns
.
but in ,
won't be droped during function calling, so, let suppose
using type = const X&.
We're actually calling in
.
of
is
, and
returns
, according to the collapsing rule,
becomes
.
Beside this, there're more errors.
if we do this in
int main(){
X v;
const X c;
f(X());
f(std::move(v));
}
It won't even compile, the error is
Sure, takes a lvalue reference as parameter, how can we pass a rvalue reference to it !!!
Perfect forwarding
And to uniform all 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 call. We actually called
, the
for
is
,
returns
, a rvalue reference to a lvalue reference, it collapsed as
according the C++ reference collapsing rule, so
actually returns
, it's straight forward that it calls
.
And when we call . We actually called
. And you're encouraged to do the rest deduction by yourself.
And last, when we call for
, we call
with a rvalue reference, we just called
, the
for
is just
.
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, 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.