Some functions need to forward one or more of their arguments with their types unchanged to another, forwarded-to, function. In such cases, we need to preserve everything about the forwarded arguments, including whether or not the argument type is const
, and whether the argument is an lvalue or an rvalue.
As an example, we’ll write a function that takes a callable expression and two additional arguments. Our function will call the given callable with the other two arguments in reverse order. The following is a first cut at our flip function:
// template that takes a callable and two parameters
// and calls the given callable with the parameters ''flipped''
// flip1 is an incomplete implementation: top-level const and references are lost
template <typename F, typename T1, typename T2>
void flip1(F f, T1 t1, T2 t2)
{
f(t2, t1);
}
This template works fine until we want to use it to call a function that has a reference parameter:
void f(int v1, int &v2) // note v2 is a reference
{
cout << v1 << " " << ++v2 << endl;
}
Here f
changes the value of the argument bound to v2
. However, if we call f
through flip1
, the changes made by f
do not affect the original argument:
f(42, i); // f changes its argument i
flip1(f, j, 42); // f called through flip1 leaves j unchanged
The problem is that j
is passed to the t1
parameter in flip1
. That parameter has is a plain, nonreference type, int
, not an int&
. That is, the instantiation of this call to flip1
is
void flip1(void(*fcn)(int, int&), int t1, int t2);
The value of j
is copied into t1
. The reference parameter in f
is bound to t1
, not to j
.
To pass a reference through our flip function, we need to rewrite our function so that its parameters preserve the “lvalueness” of its given arguments. Thinking ahead a bit, we can imagine that we’d also like to preserve the const
ness of the arguments as well.
We can preserve all the type information in an argument by defining its corresponding function parameter as an rvalue reference to a template type parameter. Using a reference parameter (either lvalue or rvalue) lets us preserve const
ness, because the const
in a reference type is low-level. Through reference collapsing (§ 16.2.5, p. 688), if we define the function parameters as T1&&
and T2&&
, we can preserve the lvalue/rvalue property of flip’s arguments (§ 16.2.5, p. 687):
template <typename F, typename T1, typename T2>
void flip2(F f, T1 &&t1, T2 &&t2)
{
f(t2, t1);
}
As in our earlier call, if we call flip2(f, j, 42)
, the lvalue j
is passed to the parameter t1
. However, in flip2
, the type deduced for T1
is int&
, which means that the type of t1
collapses to int&
. The reference t1
is bound to j
. When flip2
calls f
, the reference parameter v2
in f
is bound to t1
, which in turn is bound to j
. When f
increments v2
, it is changing the value of j
.
A function parameter that is an rvalue reference to a template type parameter (i.e., T&&
) preserves the const
ness and lvalue/rvalue property of its corresponding argument.
This version of flip2
solves one half of our problem. Our flip2
function works fine for functions that take lvalue references but cannot be used to call a function that has an rvalue reference parameter. For example:
void g(int &&i, int& j)
{
cout << i << " " << j << endl;
}
If we try to call g
through flip2
, we will be passing the parameter t2
to g
’s rvalue reference parameter. Even if we pass an rvalue to flip2
:
flip2(g, i, 42); // error: can't initialize int&& from an lvalue
what is passed to g
will be the parameter named t2
inside flip2
. A function parameter, like any other variable, is an lvalue expression (§ 13.6.1, p. 533). As a result, the call to g
in flip2
passes an lvalue to g
’s rvalue reference parameter.
std::forward
to Preserve Type Information in a CallWe can use a new library facility named forward
to pass flip2
’s parameters in a way that preserves the types of the original arguments. Like move, forward
is defined in the utility
header. Unlike move, forward
must be called with an explicit template argument (§ 16.2.2, p. 682). forward
returns an rvalue reference to that explicit argument type. That is, the return type of forward<T>
is T&&
.
Ordinarily, we use forward
to pass a function parameter that is defined as an rvalue reference to a template type parameter. Through reference collapsing on its return type, forward
preserves the lvalue/rvalue nature of its given argument:
template <typename Type> intermediary(Type &&arg)
{
finalFcn(std::forward<Type>(arg));
// ...
}
Here we use Type
—which is deduced from arg
—as forward
’s explicit template argument type. Because arg
is an rvalue reference to a template type parameter, Type
will represent all the type information in the argument passed to arg
. If that argument was an rvalue, then Type
is an ordinary (nonreference) type and forward<Type>
will return Type&&
. If the argument was an lvalue, then—through reference collapsing—Type
itself is an lvalue reference type. In this case, the return type is an rvalue reference to an lvalue reference type. Again through reference collapsing—this time on the return type—forward<Type>
will return an lvalue reference type.
When used with a function parameter that is an rvalue reference to template type parameter (T&&
), forward
preserves all the details about an argument’s type.
Using forward
, we’ll rewrite our flip function once more:
template <typename F, typename T1, typename T2>
void flip(F f, T1 &&t1, T2 &&t2)
{
f(std::forward<T2>(t2), std::forward<T1>(t1));
}
If we call flip(g, i, 42), i
will be passed to g
as an int&
and 42
will be passed as an int&&
.