Why is template argument deduction disabled with std::forward?
If you pass an rvalue reference to an object of type X
to a template function that takes type T&&
as its parameter, template argument deduction deduces T
to be X
. Therefore, the parameter has type X&&
. If the function argument is an lvalue or const lvalue, the compiler deduces its type to be an lvalue reference or const lvalue reference of that type.
If std::forward
used template argument deduction:
Since objects with names are lvalues
the only time std::forward
would correctly cast to T&&
would be when the input argument was an unnamed rvalue (like 7
or func()
). In the case of perfect forwarding the arg
you pass to std::forward
is an lvalue because it has a name. std::forward
's type would be deduced as an lvalue reference or const lvalue reference. Reference collapsing rules would cause the T&&
in static_cast<T&&>(arg)
in std::forward to always resolve as an lvalue reference or const lvalue reference.
Example:
template<typename T>
T&& forward_with_deduction(T&& obj)
{
return static_cast<T&&>(obj);
}
void test(int&){}
void test(const int&){}
void test(int&&){}
template<typename T>
void perfect_forwarder(T&& obj)
{
test(forward_with_deduction(obj));
}
int main()
{
int x;
const int& y(x);
int&& z = std::move(x);
test(forward_with_deduction(7)); // 7 is an int&&, correctly calls test(int&&)
test(forward_with_deduction(z)); // z is treated as an int&, calls test(int&)
// All the below call test(int&) or test(const int&) because in perfect_forwarder 'obj' is treated as
// an int& or const int& (because it is named) so T in forward_with_deduction is deduced as int&
// or const int&. The T&& in static_cast<T&&>(obj) then collapses to int& or const int& - which is not what
// we want in the bottom two cases.
perfect_forwarder(x);
perfect_forwarder(y);
perfect_forwarder(std::move(x));
perfect_forwarder(std::move(y));
}
Because std::forward(expr)
is not useful. The only thing it can do is a no-op, i.e. perfectly-forward its argument and act like an identity function. The alternative would be that it's the same as std::move
, but we already have that. In other words, assuming it were possible, in
template<typename Arg>
void generic_program(Arg&& arg)
{
std::forward(arg);
}
std::forward(arg)
is semantically equivalent to arg
. On the other hand, std::forward<Arg>(arg)
is not a no-op in the general case.
So by forbidding std::forward(arg)
it helps catch programmer errors and we lose nothing since any possible use of std::forward(arg)
are trivially replaced by arg
.
I think you'd understand things better if we focus on what exactly std::forward<Arg>(arg)
does, rather than what std::forward(arg)
would do (since it's an uninteresting no-op). Let's try to write a no-op function template that perfectly forwards its argument.
template<typename NoopArg>
NoopArg&& noop(NoopArg&& arg)
{ return arg; }
This naive first attempt isn't quite valid. If we call noop(0)
then NoopArg
is deduced as int
. This means that the return type is int&&
and we can't bind such an rvalue reference from the expression arg
, which is an lvalue (it's the name of a parameter). If we then attempt:
template<typename NoopArg>
NoopArg&& noop(NoopArg&& arg)
{ return std::move(arg); }
then int i = 0; noop(i);
fails. This time, NoopArg
is deduced as int&
(reference collapsing rules guarantees that int& &&
collapses to int&
), hence the return type is int&
, and this time we can't bind such an lvalue reference from the expression std::move(arg)
which is an xvalue.
In the context of a perfect-forwarding function like noop
, sometimes we want to move, but other times we don't. The rule to know whether we should move depends on Arg
: if it's not an lvalue reference type, it means noop
was passed an rvalue. If it is an lvalue reference type, it means noop
was passed an lvalue. So in std::forward<NoopArg>(arg)
, NoopArg
is a necessary argument to std::forward
in order for the function template to do the right thing. Without it, there's not enough information. This NoopArg
is not the same type as what the T
parameter of std::forward
would be deduced in the general case.