COMS W4995 C++ Deep Dive for C Programmers

Forwarding References & Variadic Templates

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.

Value categories

L-values and r-values revisited

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.

R-value reference is an l-value!

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().

Summary of value categories

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.

Forwarding Reference

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.

Perfecting forwarding

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.

Implementing forward()

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.

Variadic Templates

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 

Using forwarding references with packs

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) 

Fold expressions

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