C++ / a working model

49 / 163   ·   C++11   ·   10 min

Forwarding references and perfect forwarding

Keep this sentence

A forwarding reference, together with template deduction, records the value category the caller passed in; std::forward then uses that information for the next call. It is for transparent wrappers, not an alias for every T&&, and not a more advanced general replacement for std::move.

In this lesson
  1. First decide whether deduction is actually happening
  2. A named parameter loses the value category of the call expression
  3. Perfect forwarding has limits
  4. Example
  5. Exercise

First decide whether deduction is actually happening

A typical forwarding reference is an unqualified type parameter T&& deduced from this call in a function template. When an lvalue is passed, T can be deduced as U&; when an rvalue is passed, T is deduced as U. const T&& is not a forwarding reference; neither is a member-function parameter T&& whose T is already fixed by a class template.

After deduction, a logical “reference to reference” can appear; the language removes it with reference collapsing: if any layer is an lvalue reference, the result is an lvalue reference; only an rvalue reference combined with an rvalue reference remains an rvalue reference. A wrapper can therefore take lvalues, const lvalues, and rvalues with one declaration.

A named parameter loses the value category of the call expression

Inside the wrapper, the parameter x already has a name, so the expression x is an lvalue. A direct target(x) therefore prefers lvalue overloads for every call. std::forward<T>(x) uses the information stored in T to restore lvalue or rvalue category conditionally, while keeping const and other type properties.

The example defines three overloads for the target, returning markers for a mutable lvalue, a read-only lvalue, and an rvalue. The forwarding wrapper preserves all three choices; a second wrapper that does not forward shows that an rvalue also becomes a named lvalue once it enters the function. This makes the effect of forwarding observable without relying on allocation counts or compiler optimizations.

Perfect forwarding has limits

Forwarding fits factories, in-place construction in containers, and call-adapter layers: that layer does not decide whether to consume the object; it only faithfully passes the caller’s intent. If an interface clearly takes and consumes a known type, an ordinary T&& with std::move is more direct; if the code only needs to read data, const T& may already suffice.

“Perfect” does not mean every syntax can be forwarded transparently. Untyped brace lists, overload sets, and some bit-fields cannot be deduced as ordinary T&&. Forwarding the same argument twice can also let the first call consume a resource while the second sees a moved-from state. Let the interface’s needs decide the scope of forwarding; do not default every parameter to a universal form.

Pitfalls

  • Replacing std::forward<T>(x) with std::move(x) also forces the caller’s lvalues to be given up as rvalues, breaking the wrapper’s transparency.
  • A T&& whose type is already fixed, and const T&&, are not the forwarding references described here; two ampersands alone are not enough to tell.

Run an example

Minimum C++11 · complete program · Download .cpp

#include <cassert>
#include <iostream>
#include <utility>

int select(int&) { return 1; }
int select(const int&) { return 2; }
int select(int&&) { return 3; }
template<class T>
int relay(T&& x) { return select(std::forward<T>(x)); }
template<class T>
int named(T&& x) { return select(x); }

int main() {
    int n = 1;
    const int c = 2;
    assert(relay(n) == 1);
    assert(relay(c) == 2);
    assert(relay(3) == 3);
    assert(named(3) == 1);
    std::cout << relay(n) << ' ' << relay(c) << ' ' << relay(3) << '\n';
}

Compile locally

g++ -std=c++11 -Wall -Wextra -Wpedantic -pthread modern-forward.cpp -o example && ./example

Expected result

1 2 3

CHECK YOUR UNDERSTANDING

Close the answer. Explain it.

When calling relay(n), what are T and the final parameter type? What about relay(std::move(n))?

Show a reference answer

In the first call T is int&, T&& collapses to int&, and the result of forward is an lvalue. In the second, T is int, the parameter type is int&&, and the result of forward is an xvalue. Both calls still refer to the same n; in this example the target only returns a marker and does not actually modify or consume it.

Check the sources

Drafts and official chapters change. The version mark is only the example’s minimum.

Back to the catalog