Why Is Template Argument Deduction Disabled With Std::Forward

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));
}

Why can't std::forward deduce template parameters on his own?

The type of std::forward<T>'s argument is std::remove_reference<T>::type&. Say you pass an object of type X, the compiler knows that std::remove_reference<T>::type should be X. However, how can it determine T? It would have to instantiate std::remove_reference for every possible type (an infinite set) to find out which ones have a type of X. That's why automatic type deduction cannot be done in cases like this.

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));
}

Why std::forward() doesn't deduce type?

Consider the original use case of forward:

template<class T>
void f(T&& t) { g(std::forward<T>(t)); }

t has a name, so it's an lvalue inside f even if it's bound to an rvalue. If forward is allowed to deduce type, then people would be tempted to write std::forward(t) and not actually get the perfect forwarding they expected.


Also, your analysis is not right. template<class S> void f(S& t); doesn't bind to rvalues. std::forward is actually a pair of overloads - the one you are referring to takes lvalues only, and

template <class T> constexpr T&& forward(remove_reference_t<T>&& t) noexcept;

handles rvalues.

How remove_reference disable template argument deductions?

S in the expression typename std::remove_reference<S>::type is a non-deduced context (specifically because S appears in the nested-name-specifier of a type specified using a qualified-id). Non-deduced contexts are, as the name suggests, contexts in which the template argument cannot be deduced.

This case provides an easy example to understand why. Say I had:

int i;
forward(i);

What would S be? It could be int, int&, or int&& - all of those types would yield the correct argument type for the function. It's simply impossible for the compiler to determine which S you really mean here - so it doesn't try. It's non-deducible, so you have to explicitly provide which S you mean:

forward<int&>(i); // oh, got it, you meant S=int&

Perfect forwarding with class template argument deduction

The C++ standard defines the term forwarding reference. I suppose universal reference is used as a synonym for this term. [temp.deduct.call]/3

A forwarding reference is an rvalue reference to a cv-unqualified template parameter that does not represent a template parameter of a class template.

This concept only applies to template function argument or template constructor argument. In all other cases, T&& is a rvalue reference. The concept of forwarding reference is only usefull for template argument deduction. Let's consider that in the following examples, all the fonctions and constructors are called with an int argument (independently of its constness and value categories (lvalue/rvalue):

//possibilities of argument deduction, [cv] means any combination of "const" and "volatile": 
// <"","const","volatile","const volatile">
template<class T> void f(T&);
//4 possibilities: void f([cv] int&);

template<class T> void f(const T&);
//2 possibilities: void f(const int&);
//void f(const volatile int&);

template<class T> void f(T&&);
//Forwarding reference, 8 possibilities
//void f([cv] int&);
//void f([cv] int&&);

template<class T> void f(const T&&);
//NOT a forwarding reference because of the const qualifier, 2 possibilities:
//void f(const int&&);
//void f(const volatile int&&);

template<class T>
struct S{
template<class U>
S(U&&);
//Forwarding reference, 8 posibilities:
//void S<X>([cv] int&);
//void S<X>([cv] int&&);
//no template argument deduction posible

S(T&&);
//NOT a forwarding reference, 1 possibility:
//void S<X>(X&&);
//Generated argument deduction:
//template<class T> S(T&&) -> S<T>;
//not a forwarding reference because T is a parameter of the template class;
//=> 4 possibilities: -> S<[cv] int&&>


T&& a; //an rvalue reference if T is [cv] int or [cv] int&&,
//an lvalue reference if T is [cv] int&;
//This comes from reference colapsing rules: &+&=&; &&+&=&; &&+&&=&& //(Nota: You may consider that a rvalue reference data member is probably a mistake)
};

template<class U>
S(U&&) -> S<U&&>;
//Forwarding reference, 8 possibilities:
// S<[cv] int&>;
// S<[cv] int&&>;

Using std::forward make sense only inside the body of a function or a constructor if the argument of std::forward can either be a rvalue reference or a lvalue reference, depending on template argument deduction and reference collapsing rules. If std::forward's argument always results in a rvalue reference, std::move is prefered, and if it always results in a lvalue reference, nothing is prefered.

How does std::forward receive the correct argument?

It does bind to the overload of std::forward taking an lvalue:

template <class T>
constexpr T&& forward(remove_reference_t<T>& t) noexcept;

It binds with T == int. This function is specified to return:

static_cast<T&&>(t)

Because the T in f deduced to int. So this overload casts the lvalue int to xvalue with:

static_cast<int&&>(t)

Thus calling the g(int&&) overload.

In summary, the lvalue overload of std::forward may cast its argument to either lvalue or rvalue, depending upon the type of T that it is called with.

The rvalue overload of std::forward can only cast to rvalue. If you try to call that overload and cast to lvalue, the program is ill-formed (a compile-time error is required).

So overload 1:

template <class T>
constexpr T&& forward(remove_reference_t<T>& t) noexcept;

catches lvalues.

Overload 2:

template <class T> constexpr T&& forward(remove_reference_t<T>&& t) noexcept;

catches rvalues (which is xvalues and prvalues).

Overload 1 can cast its lvalue argument to lvalue or xvalue (the latter which will be interpreted as an rvalue for overload resolution purposes).

Overload 2 can can cast its rvalue argument only to an xvalue (which will be interpreted as an rvalue for overload resolution purposes).

Overload 2 is for the case labeled "B. Should forward an rvalue as an rvalue" in N2951. In a nutshell this case enables:

std::forward<T>(u.get());

where you are unsure if u.get() returns an lvalue or rvalue, but either way if T is not an lvalue reference type, you want to move the returned value. But you don't use std::move because if T is an lvalue reference type, you don't want to move from the return.

I know this sounds a bit contrived. However N2951 went to significant trouble to set up motivating use cases for how std::forward should behave with all combinations of the explicitly supplied template parameter, and the implicitly supplied expression category of the ordinary parameter.

It isn't an easy read, but the rationale for each combination of template and ordinary parameters to std::forward is in N2951. At the time this was controversial on the committee, and not an easy sell.

The final form of std::forward is not exactly what N2951 proposed. However it does pass all six tests presented in N2951.

Understanding of the implementation of std::forward since C++11

forward is essentially a machinery to conserve the value category in perfect forwarding.

Consider a simple function that attempts to call the f function transparently, respecting value category.

template <class T>
decltype(auto) g(T&& arg)
{
return f(arg);
}

Here, the problem is that the expression arg is always an lvalue regardless of whether arg is of rvalue reference type. This is where forward comes in handy:

template <class T>
decltype(auto) g(T&& arg)
{
return f(forward<T>(arg));
}

Consider a reference implementation of std::forward:

template <class T>
constexpr T&& forward(remove_reference_t<T>& t) noexcept
{
return static_cast<T&&>(t);
}

template <class T>
constexpr T&& forward(remove_reference_t<T>&& t) noexcept
{
static_assert(!std::is_lvalue_reference_v<T>);
return static_cast<T&&>(t);
}

(You can use decltype(auto) here, because the deduced type will always be T&&.)

In all the following cases, the first overload is called because the expression arg denotes a variable and hence is an lvalue:

  • If g is called with a non-const lvalue, then T is deduced as a non-const lvalue reference type. T&& is the same as T, and forward<T>(arg) is a non-const lvalue expression. Therefore, f is called with a non-const lvalue expression.

  • If g is called with a const lvalue, then T is deduced as a const lvalue reference type. T&& is the same as T, and forward<T>(arg) is a const lvalue expression. Therefore, f is called with a const lvalue expression.

  • If g is called with an rvalue, then T is deduced as a non-reference type. T&& is an rvalue reference type, and forward<T>(arg) is an rvalue expression. Therefore, f is called with an rvalue expression.

In all cases, the value category is respected.

The second overload is not used in normal perfect forwarding. See What is the purpose of std::forward()'s rvalue reference overload? for its usage.

How does std::forward work?

First, let's take a look at what std::forward does according to the standard:

§20.2.3 [forward] p2

Returns: static_cast<T&&>(t)

(Where T is the explicitly specified template parameter and t is the passed argument.)

Now remember the reference collapsing rules:

TR   R

T& & -> T& // lvalue reference to cv TR -> lvalue reference to T
T& && -> T& // rvalue reference to cv TR -> TR (lvalue reference to T)
T&& & -> T& // lvalue reference to cv TR -> lvalue reference to T
T&& && -> T&& // rvalue reference to cv TR -> TR (rvalue reference to T)

(Shamelessly stolen from this answer.)

And then let's take a look at a class that wants to employ perfect forwarding:

template<class T>
struct some_struct{
T _v;
template<class U>
some_struct(U&& v)
: _v(static_cast<U&&>(v)) {} // perfect forwarding here
// std::forward is just syntactic sugar for this
};

And now an example invocation:

int main(){
some_struct<int> s1(5);
// in ctor: '5' is rvalue (int&&), so 'U' is deduced as 'int', giving 'int&&'
// ctor after deduction: 'some_struct(int&& v)' ('U' == 'int')
// with rvalue reference 'v' bound to rvalue '5'
// now we 'static_cast' 'v' to 'U&&', giving 'static_cast<int&&>(v)'
// this just turns 'v' back into an rvalue
// (named rvalue references, 'v' in this case, are lvalues)
// huzzah, we forwarded an rvalue to the constructor of '_v'!

// attention, real magic happens here
int i = 5;
some_struct<int> s2(i);
// in ctor: 'i' is an lvalue ('int&'), so 'U' is deduced as 'int&', giving 'int& &&'
// applying the reference collapsing rules yields 'int&' (& + && -> &)
// ctor after deduction and collapsing: 'some_struct(int& v)' ('U' == 'int&')
// with lvalue reference 'v' bound to lvalue 'i'
// now we 'static_cast' 'v' to 'U&&', giving 'static_cast<int& &&>(v)'
// after collapsing rules: 'static_cast<int&>(v)'
// this is a no-op, 'v' is already 'int&'
// huzzah, we forwarded an lvalue to the constructor of '_v'!
}

I hope this step-by-step answer helps you and others understand just how std::forward works.



Related Topics



Leave a reply



Submit