We previously discussed l-values and r-values when we studied move semantics. We start this chapter by revisiting them in much greater depth to prepare ourselves to learn about forwarding references. We’ll also study variadic templates that are often used in tandem with forwarding references. A solid understsanding of forwarding references and variadic templates is crucial for parsing many STL API specifications.
Consider the following program, 16/values:
// Wrapper for heap-allocated double
struct D {
D(double x = -1.0) : p{new double(x)} { cout << "(ctor:" << *p << ") "; }
~D() { cout << "(dtor:" << (p ? *p : 0) << ") "; delete p; }
D(const D& d) : p{new double(*d.p)} { cout << "(copy:" << *p << ") "; }
D(D&& d) : p{d.p} { d.p = nullptr; cout << "(move:" << *p << ") "; }
D& operator=(const D& d) = delete;
D& operator=(D&& d) = delete;
operator double&() { return *p; }
operator const double&() const { return *p; }
double* p;
};
int main() {
// 1. Binding lvalue
D d1 { 1.0 };
D& rd1 = d1;
const D& crd1 = d1;
// D&& rrd1 = d1; // err: cannot bind lvalue to rvalue reference
// 2. Binding rvalue
// D& rd2 = D(2.0); // err: cannot bind rvalue to lvalue reference
const D& crd3 = D(3.0);
D&& rrd4 = D(4.0);
// the temp object is mutable through rvalue ref
rrd4 += 0.1;
// and both temp objects are still alive!
cout << "[" << crd3 << "," << rrd4 << "] ";
cout << "(bye) ";
}
First, we have a class D, a simple wrapper around a heap-allocated double that
logs constructor, destructor, copy constructor, and move constructor calls. Its
implementations of operator double&() and operator const double&() allows a
seamless conversion to double.
The first block of the main() function reviews our understanding of binding an
l-value to various references. The named object d1, which is an l-value, can
be bound to an l-value reference, rd1, and a const l-value reference, crd1,
but it can’t be bound to an r-value reference, rrd1.
The second block of the main() function tries to do the same for temporary
r-value objects. We are unable to bind a temporary object to an l-value
reference rd2. But we can bind a temporary object to a const l-value
reference, crd3, as well as to an r-value reference, rrd4. So far, this has
all been a review of our discussion of l-values and r-values when we covered
move semantics.
The program prints out the values of crd3 and rrd4 using operator<<().
This works without operator<<() for our D class because we implemented
conversion operators to double. The output is shown below:
(ctor:1) (ctor:3) (ctor:4) [3,4.1] (bye) (dtor:4.1) (dtor:3) (dtor:1)
The three constructor calls are for d1, D(3.0), and D(4.0), respectively.
We increment the D(4.0) object through the r-value reference, rrd4, so we
see the updated value 4.1 being printed. This demonstrates that temporary
unnamed object bound to an r-value reference is mutable. Finally, we see the
three objects are destructed after (bye) is logged at the end of the main()
function. This demonstrates that, when a temporary object is bound to a
reference, the object’s lifetime is extended to the lifetime of the reference.
The code snippet below demonstrates an important and confusing fact about r-value references:
int main() {
...
D&& rrd4 = D(4.0);
...
// 3. Binding rvalue reference to another rvalue reference
// D&& rrd4_1 = rrd4; // err: rvalue reference itself is an lvalue!
D& rrd4_2 = rrd4; // ok: rvalue reference itself is an lvalue!
D&& rrd4_3 = std::move(rrd4); // ok: cast rrd4 back to rvalue again
cout << "(bye) ";
}
Surprisingly, we cannot assign an r-value reference, rrd4, to another r-value
reference, rrd4_1. The compiler says that we can’t assign bind an r-value
reference to an l-value. But where is the l-value? It turns out that an r-value
reference is itself an l-value! Generally speaking, anything that has a name,
like a variable, whether it is an object, pointer, or reference, is an l-value.
Since rrd4 is a reference type variable with a name, it is an l-value. In
fact, we can bind an r-value reference, rrd4, to a l-value reference, rrd4_2
because it is an l-value. Recall that rrd4 was bound to the temporary object
D(4.0) which is now bound to an l-value reference, rrd4_2. We were able to
bind a temporary object to an l-value reference using an intermediary r-value
reference. This is a consequence of treating all named variables as l-values. If
we would like to assign rrd4 to another r-value reference, we must explicitly
cast to an r-value reference using std::move().
Every C++ expression has a value category. The classical distinction between l-values and r-values still holds.
Consider the following snippet:
int i = 0;
std::string s{"hello"};
i is an example of an l-value because of it is a named variable. The
expression s[i] is also an l-value because it’s a function call,
operator[](), that returns an l-value reference.
We can divide r-values into two sub-categories. prvalues, which stands for
pure r-values, are traditional r-values such as integer literals, temporary
unnamed objects, and function calls returning by value. Some examples are 2.0,
s + s (where s is a string), and string{"abc"}.
xvalues, which stands for expiring values, is another sub-category of
r-values. xvalue expressions include function calls that return r-value
references or expressions casted to r-value references. The prime example of an
xvalue expression is a call to std::move().
One surprising fact is that C string literals, like "abc" are considered
l-values instead of prvalues. While a C string literal is an unnamed array, it
does have a permanent storage location, allowing us to take the address of it,
effectively giving the string literal an identity.
While an xvalue is officially considered an r-value, it differs from prvalues in
the sense that an xvalue has an identity. Consider the expression
std::move(s). The term “xvalue” comes from the fact that the resources owned
by object s are being transferred to another object, so s is therefore
considered to be expiring soon. So an xvalue is similar to an unnamed temporary
object in that it’s about to go away, but the difference is that it clearly has
an identity.
For this reason, C++ expressions can also be categorized as glvalues vs. prvalues. glvalues, which stands for generalized l-values, is the union of l-values and xvalues. prvalues are, as we previously said, r-values that are not xvalues.
Consider the following program 16/fwd-ref:
// Same struct D definition from 16/values.cpp
struct D { ... };
template <typename T> void f1(T t) { t += 0.1; }
template <typename T> void f2(T& t) { t += 0.2; }
int main() {
// 1. f1: we can pass lvalue or rvalue but t is a copy
cout << "\ntemplate <typename T> void f1(T t) { t += 0.1; }\n";
{
D d1 {1.0};
f1(d1);
f1(D(2.0));
}
cout << '\n';
// 2. f2: cannot pass rvalue
cout << "\ntemplate <typename T> void f2(T& t) { t += 0.2; }\n";
{
D d1 {1.0};
f2(d1);
// f2(D(2.0)); // err: cannot bind rvalue to T&
}
cout << '\n';
}
The program produces the following output:
template <typename T> void f1(T t) { t += 0.1; }
(ctor:1) (copy:1) (dtor:1.1) (ctor:2) (dtor:2.1) (dtor:1)
template <typename T> void f2(T& t) { t += 0.2; }
(ctor:1) (dtor:1.2)
The output is straightforward. f1() takes t by value. We pass an l-value
d1 and see a copy construction as a result. When we pass an r-value D{2.0},
the compiler elides the copy and constructs the object directly for the
parameter t. f2() takes t by l-value reference, which means we can’t pass
the r-value D{2.0} anymore.
You may think the T&& t in the following function f3() means that we can
only pass r-values, but that’s not actually the case!
...
template <typename T> void f3(T&& t) { t += 0.3; }
int main() {
...
// 3. f3: forwarding reference
cout << "\ntemplate <typename T> void f3(T&& t) { t += 0.3; }\n";
{
D d1 {1.0};
f3(d1);
f3(D(2.0));
}
cout << '\n';
}
It turns out that f3() accepts both l-value and r-value references. The
example produces the following output:
template <typename T> void f3(T&& t) { t += 0.3; }
(ctor:1) (ctor:2) (dtor:2.3) (dtor:1.3)
C++ redefines the meaning of T&& in a function template like f3(). The
T&& t parameter is called a forwarding reference if T is a type
parameter of a function template. A forwarding reference becomes an r-value
reference when an r-value is passed in. In the example of f3(D{2.0}), T
resolves to D, so T&& becomes D&&.
On the other hand, a forwarding reference becomes an l-value reference when an
l-value is passed in. In the example of f3(d1), T actually resolves to D&
and T&& becomes D& &&, which collapses to D&. C++ collapses references as
follows in templates and type aliases:
X& & collapses to X&
X& && collapses to X&
X&& & collapses to X&
X&& && collapses to X&&
Forwarding references allow you to accept an l-value as an l-value reference and
an r-value as an r-value reference. Suppose now we’d like to pass along the
forwarding reference to another function. In the example below, we construct a
new D object using the forwarding reference. We expect that the copy
constructor will be invoked if t is an l-value reference and that the move
constructor will be invoked if t is an r-value reference.
...
template <typename T> void f4(T&& t) { D d{t}; d += 0.4; }
int main() {
...
// 4. f4: a forwarding reference is always an lvalue
cout << "\ntemplate <typename T> void f4(T&& t) { D d{t}; d += 0.4; }\n";
{
D d1 {1.0};
f4(d1); // t is lvalue inside f4
f4(D(2.0)); // t is also lvalue inside f4
}
cout << '\n';
}
The program’s output reveals that the copy constructor was invoked in both cases:
template <typename T> void f4(T&& t) { D d{t}; d += 0.4; }
(ctor:1) (copy:1) (dtor:1.4) (ctor:2) (copy:2) (dtor:2.4) (dtor:2) (dtor:1)
Recall from our discussion of value categories above that an r-value reference
itself is actually an l-value. Thus, a forwarding reference, whether it is an
l-value reference or r-value reference, is always an l-value. When we pass the
forwarding reference to D’s constructor, the copy constructor always gets
invoked.
How then can we forward a forwarding reference so that its l-value/r-value-ness
is preserved? C++ provides std::forward<T>() for this purpose:
...
template <typename T> void f5(T&& t) { D d{std::forward<T>(t)}; d += 0.5; }
int main() {
...
// 5. f5: perfect-forwarding a forwarding reference
cout << "\ntemplate <typename T> "
"void f5(T&& t) { D d{std::forward<T>(t)}; d += 0.5; }\n";
{
D d1 {1.0};
f5(d1);
f5(D(2.0));
}
cout << '\n';
}
The example produces the following output:
template <typename T> void f5(T&& t) { D d{std::forward<T>(t)}; d += 0.5; }
(ctor:1) (copy:1) (dtor:1.5) (ctor:2) (move:2) (dtor:2.5) (dtor:0) (dtor:1)
As we can see, the move constructor is invoked instead of the copy constructor
when t is an r-value reference – i.e., when we invoked f5(D{2.0}).
std::forward<T>() allows us to forward a forwarding reference without losing
its original value category. This is referred to as perfecting forwarding.
Here is a simplifed implementation of std::forward<T>():
template<typename T> struct remove_reference { using type = T; };
template<typename T> struct remove_reference<T&> { using type = T; };
template<typename T> struct remove_reference<T&&> { using type = T; };
template<typename T>
using remove_reference_t = typename remove_reference<T>::type;
template<typename T>
T&& forward(remove_reference_t<T>& t) noexcept {
return static_cast<T&&>(t);
}
First off, STL provides remove_reference class template and specializations
that define a nested alias type that sheds any references from its template
type parameter. For example, remove_reference<D&>::type evaluates to D. For
convenience, we define remove_reference_t alias to the nested type alias.
Let’s now understand how forward() works. Consider what happens when we invoke
f5(d1). As we said previously, f5()’s T type parameter resolves to D&,
so T&& resolves to D& &&, which collapses to D&. f5() then invokes
forward<D&>(t), which is instantiated as follows:
D& forward<D&>(D& t) {
return static_cast<D&>(t);
}
Note that the parameter type was remove_reference_t<D&>&, which resolves to
D&. As we can see, forward() preserves the l-valueness of t.
Now consider what happens when we invoke f5(D{2.0}). As we said previously,
f5()’s T type parameter resolves to D, so T&& resolves to D&&. f5()
then invokes forward<D>(t), which is instantiated as follows:
D&& forward<D>(D& t) {
return static_cast<D&&>(t);
}
Note that the parameter type this time was remove_reference_t<D>&, which still
resolves to D&. In this case, t is an r-value reference and our goal is to
turn it back into an r-value. This instantiation accepts the r-value reference
t as a D& (which is ok because r-value reference is an l-value) but casts it
back to D&& before returning it.
The upshot is that std::forward<T>(t) returns an l-value reference if t was
an l-value reference and returns an r-value reference if t was an r-value
reference.
The real std::forward<T>() implementation provides an additional overload that
takes an r-value:
template <typename T>
T&& forward(remove_reference_t<T>&& t) noexcept;
This overload handles some edge cases of forwarding the result of an expression rather than forwarding a forwarding reference. The implementation is the same, but we omit the discussion for simplicity.
Forwarding references are often used in variadic templates to write template code that can accept any number of arguments of any value category. A variadic template is a template that has a parameter pack.
In the program 16/variadic shown below, we start with a variadic function
template print_v1() that can print any number of arguments:
// Same struct D definition from 16/values.cpp
struct D { ... };
void print_v1() { cout << '\n'; } // base case for template recursion
template <typename T, typename... MoreTs> // template parameter pack
void print_v1(T arg0, MoreTs... more_args) { // function parameter pack
cout << arg0 << ' ';
// pack expansion of a pattern containing function parameter pack
print_v1(more_args...);
}
int main() {
string s { "Hi" };
print_v1(s, "ABC", 45, string{"xyz"});
cout << '\n';
print_v1(s, "ABC", 45, string{"xyz"}, D{3.14});
}
The type parameters of the function template print_v1() are T and a template
parameter pack MoreTs, which indicates zero or more additional types.
The function template declares one parameter, arg0 of type T, and declares
the rest of the parameters as a function parameter pack, more_args of type
MoreTs.... The compiler will resolve the remaining arguments and their types
from a callsite. For example, the first invocation of print_v1() in main()
will cause the compiler to generate the following instantation of print_v1():
void print_v1(string arg0, const char* arg1, int arg2, string arg3) {...}
After printing arg0, the function template makes a recursive call with the
rest of the arguments, causing the compiler to generate an instance of the
function template with one fewer parameter. The syntax used in
print_v1(more_args...) is known as pack expansion. more_args... will be
expanded into a comma-separated list of the arguments in the pack. The process
of template instantiations continues until the parameter pack is depleted, as
follows:
void print_v1(const char* arg0, int arg1, string arg2) {...}
void print_v1(int arg0, string arg1) {...}
void print_v1(string arg0) {...}
The final recursive call with an empty parameter pack will invoke the base case
specialization of print_v1() that we provided which takes no parameters. It
terminates the series of calls by just printing the newline character. Here is
the line of output produced by that first invocation of print_v1() in the
main():
Hi ABC 45 xyz
In the second invocation of print_v1() in main(), we added an additional
argument D{3.14} at the end, which produces the following output:
(ctor:3.14) Hi (copy:3.14) ABC (copy:3.14) 45 (copy:3.14) xyz (copy:3.14) 3.14
(dtor:3.14) (dtor:3.14) (dtor:3.14) (dtor:3.14) (dtor:3.14)
As we can see, the D object is being copied at each recursive call because
every overload take its arguments by value. We pass a mixture of l-values and
r-values to print_v1(). If want to avoid copies, we can use forwarding
references as follows:
void print_v2() { cout << '\n'; }
template <typename T, typename... MoreTs>
void print_v2(T&& arg0, MoreTs&&... more_args) { // forwarding reference
cout << arg0 << ' ';
// use std::forward on each arg in the parameter pack
print_v2(std::forward<MoreTs>(more_args)...);
}
int main() {
string s { "Hi" };
print_v2(s, "ABC", 45, string{"xyz"}, D{3.14});
}
print_v2() takes its parameters as forwarding references – i.e., it takes
T&& and MoreTs&&... rather than T and MoreTs.... In the recursive call,
we changed the pack expansion to std::forward<MoreTs>(more_args).... A pack
expansion can be applied not only to a pack name, but also to a pattern
containing a pack name. For example, inside the invocation of
print_v2(45, string{"xyz"}, D{3.14}), the recursive call will look like this:
print_v2(forward<string>(arg1), forward<D>(arg2)).
As expected, there are no copies shown in the output:
(ctor:3.14) Hi ABC 45 xyz 3.14
(dtor:3.14)
We can rewrite the function template using fold expressions introduced in C++17:
template <typename... Types>
void print_v3(Types&&... args) {
// Binary left fold without printing spaces
(cout << ... << args) << '\n';
// Unary right fold with comma operator to print spaces
((cout << args << ' '), ...) << '\n';
}
int main() {
string s { "Hi" };
print_v3(s, "ABC", 45, string{"xyz"}, D{3.14});
}
The program produces the following output:
(ctor:3.14) HiABC45xyz3.14
Hi ABC 45 xyz 3.14
(dtor:3.14)
First off, print_v3() is not a recursive function template so we don’t
separate the first parameter from the rest. The first statement is an example of
a binary left fold, which takes the following form:
(init_expr binary_op ... binary_op pack). It expands to
(((init_expr binary_op arg0) binary_op arg1) binary_op arg2), and so on. In
our case, (cout << ... << args) expands to
(((cout << arg0) << arg1) << arg2), and so on. The first line of the output
shows the result of the binary left fold. All the arguments are concatenated
without spaces.
There are four kinds of fold expressions:
(... binary_op pack) // unary left fold
(pack binary_op ...) // unary right fold
(init_expr binary_op ... op pack) // binary left fold
(pack binary_op ... op init_expr) // binary right fold
The second statement in print_v3() uses a unary right fold to print out our
parameter pack with spaces added in between the arguments, as shown by the
second line of the output. A unary right fold expands to
(arg0 binary_op (arg1 binary_op (arg2))), and so on. The binary
operator in our case is actually the comma operator, which takes the form
expr1, expr2. It evaluates expr1, then expr2; and its resulting value is
the value of expr2. The expression ((cout << args << ' '), ...) expands to
the following:
((cout << arg0 << ' '), ((cout << arg1 << ' '), ((cout << arg2 << ' '))))
Fold expressions provide a concise mechanism to apply binary operations to a parameter pack, improving readability in some variadic templates. As we’ve seen, fold expressions can also offer an alternative to recursive function templates.
Last updated: 2025-11-19