According to the C++23 standard, what is the output of this program?
#include <iostream>
#include <type_traits>
#include <utility>
void g(int&) { std::cout << 'L'; }
void g(int&&) { std::cout << 'R'; }
template<typename T>
void f(T&& t) {
if (std::is_same_v<T, int>) { std::cout << 1; }
if (std::is_same_v<T, int&>) { std::cout << 2; }
if (std::is_same_v<T, int&&>) { std::cout << 3; }
g(std::forward<T>(t));
}
int main() {
f(42);
int i = 0;
f(i);
}Reference collapsing ensures that int&&& turns into int&, while int&& stays as int&&.
1R2L
42 is an rvalue, i is an lvalue. How does this impact the template parameter T and the type of t?
The first thing to do when deducing a template parameter P is to ignore references:
If
Pis a reference type, the type referred to byPis used for type deduction.
In our case, P is T&&, which is indeed a reference type. So T is used for type deduction. 42 is an int, T is deduced to int, and 1 is printed.
There is however this additional rule:
A forwarding reference is an rvalue reference to a cv-unqualified template parameter (...). If
Pis a forwarding reference and the argument is an lvalue, the type “lvalue reference toA” is used in place ofAfor type deduction.
(cv-unqualified just means no const or volatile.)
T&& is indeed an rvalue reference to a cv-unqualified template parameter, so it is a forwarding reference.
In the case of f(42) which we saw above, the argument was not an lvalue, so this rule did not apply.
In the case of f(i) however, i is an lvalue, and the rule applies. int& is used in place of int for type deduction. T is deduced to int&, and 2 is printed.
Now, which overload of g is called in the two cases?
In both cases, t itself is an lvalue, so without the call to std::forward, g(int&) would be called. With the use of std::forward however, we can change that.
§[forward]¶3 explains what std::forward does:
template<class T> constexpr T&& forward(remove_reference_t<T>& t) noexcept; template<class T> constexpr T&& forward(remove_reference_t<T>&& t) noexcept;Mandates: For the second overload,
is_lvalue_reference_visfalse.Returns:
static_cast.(t)
In the case of f(42), T is int, and std::forward returns static_cast. The result is an rvalue, g(int&&) is selected, and R is printed.
In the case of f(i), T is int&, and std::forward would return static_cast. However, int&&& t collapses to int& (see [1] below), and it returns static_cast instead. The result is an lvalue, g(int&) is selected, and L is printed.
[1] Reference collapsing
Reference collapsing explains how int&&& above collapsed to int&. The mechanism is defined in the section about typedef-names:
If a typedef-name (...) denotes a type
TRthat is a reference to a typeT, an attempt to create the type “lvalue reference to cvTR” creates the type “lvalue reference toT”, while an attempt to create the type “rvalue reference to cvTR” creates the typeTR.[Note 3: This rule is known as reference collapsing. — end note]
But what does template parameters have to do with typedef-names? It turns out that template-parameters are typedef-names: §[temp.param]¶3:
A type-parameter whose identifier does not follow an ellipsis defines its identifier to be a typedef-name (...) in the scope of the template declaration.
So reference collapsing applies to template parameters as well.
In the case of f(i), T is int&, and we're doing T&&. So according to the quote above, our TR is int&, and we're attempting to create the type “rvalue reference to int&”, which creates the type int&.