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, thenT
is deduced as a non-const lvalue reference type.T&&
is the same asT
, andforward<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, thenT
is deduced as a const lvalue reference type.T&&
is the same asT
, andforward<T>(arg)
is a const lvalue expression. Therefore,f
is called with a const lvalue expression.If
g
is called with an rvalue, thenT
is deduced as a non-reference type.T&&
is an rvalue reference type, andforward<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
Throwing Exceptions from Constructors
Why Would One Use Nested Classes in C++
How to Deal With Mutexes in Movable Types in C++
High Resolution Timer With C++ and Linux
Dividing Two Integers to Produce a Float Result
How to Get a Process Handle by Its Name in C++
Linux: Executing Child Process With Piped Stdin/Stdout
How to Call a Function on All Variadic Template Args
Nice Way to Append a Vector to Itself
How Does the Ampersand(&) Sign Work in C++
C++ Preprocessor #Define-Ing a Keyword. Is It Standards Conforming
Why Does C++ Output Negative Numbers When Using Modulo
Should the Exception Thrown by Boost::Asio::Io_Service::Run() Be Caught
Heterogeneous Containers in C++
Passing a String Literal as a Type Argument to a Class Template