Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why is template argument deduction disabled with std::forward?

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?

like image 757
David Avatar asked Oct 15 '11 18:10

David


People also ask

What is template argument deduction in C++?

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.

What does std :: forward do C++?

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.

What do std :: move and std :: forward do?

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.


2 Answers

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)); } 
like image 132
David Avatar answered Sep 23 '22 04:09

David


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.

like image 36
Luc Danton Avatar answered Sep 24 '22 04:09

Luc Danton