In VS2010 std::forward is defined as such:
template<class _Ty> inline _Ty&& forward(typename identity<_Ty>::type& _Arg) { // forward _Arg, given explicitly specified type parameter return ((_Ty&&)_Arg); }
identity
appears to be used solely to disable template argument deduction. What's the point of purposefully disabling it in this case?
Class Template Argument Deduction (CTAD) is a C++17 Core Language feature that reduces code verbosity. C++17's Standard Library also supports CTAD, so after upgrading your toolset, you can take advantage of this new feature when using STL types like std::pair and std::vector.
The std::forward function as the std::move function aims at implementing move semantics in C++. The function takes a forwarding reference. According to the T template parameter, std::forward identifies whether an lvalue or an rvalue reference has been passed to it and returns a corresponding kind of reference.
std::move takes an object and casts it as an rvalue reference, which indicates that resources can be "stolen" from this object. std::forward has a single use-case: to cast a templated function parameter of type forwarding reference ( T&& ) to the value category ( lvalue or rvalue ) the caller used to pass it.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With