Rvalue References and Perfect Forwarding

Master rvalue references and perfect forwarding in C++ — learn T&&, universal references, std::forward, reference collapsing, and how to write efficient generic code.

Rvalue References and Perfect Forwarding

In C++11, rvalue references (written as T&&) are a new reference type that binds exclusively to temporary (rvalue) expressions, enabling move semantics by allowing functions to “steal” resources from temporaries. Perfect forwarding is the technique of passing function arguments to another function while preserving their exact value category — lvalue as lvalue, rvalue as rvalue — using std::forward<T> with universal references (also written T&& in deduced template contexts). Together, these two features form the backbone of efficient, generic C++ code.

Introduction

C++11 introduced rvalue references as part of the same proposal that brought move semantics to the language. The two concepts are deeply intertwined: move semantics relies on rvalue references to identify and bind to temporaries, while rvalue references themselves are meaningless without move operations to act on them.

But rvalue references serve a second purpose that is equally important and arguably more subtle: perfect forwarding. In generic code — templates that wrap, adapt, or compose other functions — a common and frustrating problem existed before C++11: forwarding function arguments to inner functions always lost the value category information. An rvalue argument would become an lvalue by the time it reached the inner function, forcing unnecessary copies instead of efficient moves.

Perfect forwarding solves this precisely. Using the combination of universal references (T&& in deduced contexts) and std::forward<T>, a wrapper function can pass each argument to an inner function with its original value category intact. This enables factory functions, emplace operations, wrappers, and adapters to be as efficient as if the user had called the inner function directly.

This article builds a complete, ground-up understanding of rvalue references and perfect forwarding. You will understand why T&& means two very different things depending on context, how reference collapsing makes universal references work, what std::forward actually does at the assembly level, and how to apply these techniques correctly in real generic code. All concepts are illustrated with practical examples and thorough explanations.

Rvalue References: The Foundation

An rvalue reference is declared with && after the type name. Unlike a regular lvalue reference (T&), which binds to lvalues, an rvalue reference binds exclusively to rvalues — expressions that are temporaries, literals, or objects explicitly converted to rvalues via std::move.

C++
#include <iostream>
#include <string>
using namespace std;

void acceptLvalue(string& s) {
    cout << "lvalue ref: " << s << endl;
}

void acceptRvalue(string&& s) {
    cout << "rvalue ref: " << s << endl;
    // s is an rvalue reference — we can modify or move from it
    s += " [modified inside]";
    cout << "after modification: " << s << endl;
}

int main() {
    string hello = "Hello";

    // Lvalue reference: binds to named variable
    acceptLvalue(hello);
    cout << "hello is still: " << hello << endl;

    // Rvalue reference: binds to temporary
    acceptRvalue(string("World"));  // Temporary — rvalue
    acceptRvalue(move(hello));      // Explicit cast — rvalue

    // hello may be in unspecified state after move
    cout << "hello after move: '" << hello << "'" << endl;

    // Error demonstrations (commented out):
    // acceptLvalue(string("temp"));  // ERROR: non-const lvalue ref can't bind to rvalue
    // acceptRvalue(hello);           // ERROR: rvalue ref can't bind to lvalue

    return 0;
}

Output:

C++
lvalue ref: Hello
hello is still: Hello
rvalue ref: World
after modification: World [modified inside]
rvalue ref: Hello
after modification: Hello [modified inside]
hello after move: ''

Step-by-step explanation:

  1. acceptLvalue(string& s) takes a plain lvalue reference. It can read and modify the original object but cannot bind to temporaries. acceptLvalue(string("temp")) would be a compile error.
  2. acceptRvalue(string&& s) takes an rvalue reference. It binds to string("World") — a temporary created for the call — and to move(hello) — an explicit rvalue cast. Inside the function, s refers to the rvalue’s storage.
  3. Even though s is an rvalue reference parameter, within the function body it is a named variable and therefore an lvalue. You can modify it (s += ...), read it multiple times, and take its address.
  4. After acceptRvalue(move(hello)), the string hello had its internal buffer transferred into s inside the function. When the function modifies s, it modifies the transferred buffer. When the function ends, that buffer is destroyed along with s. hello is left empty.
  5. The key insight: rvalue references allow functions to opt into receiving temporaries explicitly. The function signature T&& is a promise to the caller: “I will accept a temporary and I may consume its resources.”

The Critical Distinction: T&& in Two Contexts

Here is one of the most important — and most commonly misunderstood — facts about && in C++:

T&& means two different things depending on whether T is deduced:

  • When the type is explicitly specified (not deduced), T&& is a plain rvalue reference that only binds to rvalues.
  • When the type is deduced by the compiler (template parameter or auto), T&& is a universal reference (also called a forwarding reference) that binds to both lvalues AND rvalues.
C++
#include <iostream>
#include <string>
using namespace std;

// CONTEXT 1: Explicit type — plain rvalue reference
void onlyRvalue(string&& s) {
    cout << "plain rvalue ref: " << s << endl;
}

// CONTEXT 2: Deduced type — universal reference (binds to both!)
template<typename T>
void universalRef(T&& s) {
    cout << "universal ref, T = " << typeid(T).name() << endl;
}

int main() {
    string hello = "Hello";

    // Plain rvalue reference: only rvalues
    onlyRvalue(string("temp"));   // OK: temporary is rvalue
    onlyRvalue(move(hello));      // OK: explicit rvalue cast
    // onlyRvalue(hello);          // ERROR: hello is lvalue

    cout << "---" << endl;

    hello = "Hello";  // Restore

    // Universal reference: accepts both
    universalRef(hello);             // OK: lvalue — T deduced as string&
    universalRef(string("temp"));    // OK: rvalue — T deduced as string
    universalRef(move(hello));       // OK: rvalue cast — T deduced as string
    universalRef(42);                // OK: int literal — T deduced as int

    return 0;
}

Output:

Plaintext
plain rvalue ref: temp
plain rvalue ref: Hello
---
universal ref, T = NSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEE& (or similar)
universal ref, T = NSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEE
universal ref, T = NSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEE
universal ref, T = i

Step-by-step explanation:

  1. onlyRvalue(string&& s)string is fully specified; there is nothing to deduce. This is a plain rvalue reference. It refuses lvalues.
  2. universalRef(T&& s)T is a template parameter that the compiler deduces from the argument. When hello (an lvalue) is passed, T is deduced as string&. When a temporary is passed, T is deduced as string (no reference).
  3. The type shown for lvalue hello includes & in the type name, confirming that T was deduced as a reference type. The type for the temporary does not include &.
  4. The practical test for a universal reference: look for T&& where T is in the process of being deduced right at that point. The following are not universal references even though they use &&:
    • vector<T>::push_back(T&&)T was already deduced when the class was instantiated
    • void f(const T&&)const qualification prevents universal reference behavior
    • void f(string&&) — explicit type, not deduced

Reference Collapsing: The Mechanism Behind Universal References

Universal references work because of C++’s reference collapsing rules. When a reference is formed to a reference type, the two references are combined into one according to these rules:

Inner referenceOuter referenceResult
T&&T&
T&&&T&
T&&&T&
T&&&&T&&

The key rule: any combination involving at least one lvalue reference collapses to an lvalue reference. Only && + && stays as &&.

This is what makes universal references work:

C++
template<typename T>
void universalRef(T&& arg) {
    // When called with lvalue of type string:
    //   T is deduced as string&
    //   Parameter type becomes: string& && = string& (lvalue ref)
    //
    // When called with rvalue of type string:
    //   T is deduced as string
    //   Parameter type becomes: string && = string&& (rvalue ref)
}
C++
#include <iostream>
#include <type_traits>
using namespace std;

template<typename T>
void showRefType(T&& arg) {
    if constexpr (is_lvalue_reference_v<T>) {
        cout << "T is lvalue ref → parameter is lvalue ref (T& &&  → T&)" << endl;
    } else if constexpr (is_rvalue_reference_v<T>) {
        cout << "T is rvalue ref → parameter is rvalue ref (T&& && → T&&)" << endl;
    } else {
        cout << "T is plain type → parameter is rvalue ref (T   && → T&&)" << endl;
    }
}

int main() {
    int x = 10;
    int& lref = x;
    int&& rref = 42;  // rref is an rvalue ref, but it names the temporary

    showRefType(x);       // T = int& → lvalue ref
    showRefType(lref);    // T = int& → lvalue ref (same: lref is lvalue)
    showRefType(42);      // T = int  → rvalue ref
    showRefType(move(x)); // T = int  → rvalue ref

    return 0;
}

Output:

C++
T is lvalue ref → parameter is lvalue ref (T& &&  → T&)
T is lvalue ref → parameter is lvalue ref (T& &&  → T&)
T is plain type → parameter is rvalue ref (T   && → T&&)
T is plain type → parameter is rvalue ref (T   && → T&&)

Step-by-step explanation:

  1. showRefType(x)x is an lvalue of type int. T is deduced as int&. The parameter type int& && collapses to int&. The function receives an lvalue reference.
  2. showRefType(lref)lref is itself an lvalue (it has a name). Same deduction as above: T = int&, parameter = int&.
  3. showRefType(42)42 is a prvalue (no type modifier). T is deduced as int. The parameter type int && is just int&&. The function receives an rvalue reference.
  4. showRefType(move(x))move(x) produces an int&& (xvalue). Like case 3, T is deduced as int, parameter is int&&.
  5. Reference collapsing is not something you write — it is an implicit rule the compiler applies. Understanding it explains why universal references work and why std::forward can reconstruct the original value category.

The Problem Perfect Forwarding Solves

Before perfect forwarding existed, writing generic wrapper functions correctly was nearly impossible without resorting to multiple overloads.

Consider a simple factory function that creates objects by forwarding constructor arguments:

C++
#include <iostream>
#include <string>
using namespace std;

class Widget {
public:
    explicit Widget(string name) {
        cout << "Widget(string): " << name << endl;
    }
    Widget(string name, int id) {
        cout << "Widget(string, int): " << name << " id=" << id << endl;
    }
};

// NAIVE attempt: always treats arguments as lvalues inside the function
template<typename T, typename Arg>
T createNaive(Arg arg) {
    return T(arg);  // arg is always an lvalue here — always copies!
}

// What about rvalue args? Now we need a second overload:
template<typename T, typename Arg>
T createNaiveRvalue(Arg&& arg) {
    return T(move(arg));  // Always moves — even when caller passed lvalue!
}
// Neither is correct for both cases.
// With 2 arguments, we'd need 4 overloads (LL, LR, RL, RR).
// With N arguments: 2^N overloads. This doesn't scale.

int main() {
    string name = "Button";

    // Naive version: always copies, even when rvalue passed
    auto w1 = createNaive<Widget>(name);           // copy of name into arg, then into Widget
    auto w2 = createNaive<Widget>(string("Icon")); // copy of temporary into arg

    return 0;
}

Output:

C++
Widget(string): Button
Widget(string): Icon

The naive version works but is inefficient. With createNaive, every argument is copied when passing into arg, and then potentially copied again when forwarding to T. With a 2-argument version, you would need four overloads to handle every combination of lvalue and rvalue. With 5 arguments, you would need 32 overloads.

This combinatorial explosion was a real limitation in C++03 library code. Perfect forwarding solves it with a single template.

std::forward: The Mechanics

std::forward<T> is a conditional cast that preserves the value category of an argument:

  • If T is an lvalue reference type (deduced when an lvalue was passed), forward<T>(arg) returns an lvalue reference — preserving the lvalue nature.
  • If T is a non-reference type (deduced when an rvalue was passed), forward<T>(arg) returns an rvalue reference — restoring the rvalue nature.
C++
// Simplified implementation of std::forward
template<typename T>
T&& forward(remove_reference_t<T>& arg) noexcept {
    return static_cast<T&&>(arg);
}

When T = string& (lvalue case):

  • static_cast<string& &&>(arg) collapses to static_cast<string&>(arg) → returns lvalue reference.

When T = string (rvalue case):

  • static_cast<string&&>(arg) → returns rvalue reference.

So std::forward<T> is really static_cast<T&&> — it casts to T&&, and reference collapsing does the rest.

Perfect Forwarding in Action

Here is the correct factory function using perfect forwarding:

C++
#include <iostream>
#include <string>
#include <memory>
using namespace std;

class Widget {
    string name_;
    int    id_;
public:
    // Various constructors
    explicit Widget(const string& name)
        : name_(name), id_(0) {
        cout << "Widget(const string&): '" << name_ << "' — copy" << endl;
    }

    explicit Widget(string&& name)
        : name_(move(name)), id_(0) {
        cout << "Widget(string&&): '" << name_ << "' — move" << endl;
    }

    Widget(string name, int id)
        : name_(move(name)), id_(id) {
        cout << "Widget(string, int): '" << name_ << "' id=" << id_ << endl;
    }

    void describe() const {
        cout << "Widget: '" << name_ << "' id=" << id_ << endl;
    }
};

// Perfect forwarding factory: single template handles all cases
template<typename T, typename... Args>
T createPerfect(Args&&... args) {
    return T(forward<Args>(args)...);
}

int main() {
    string btnName = "Button";

    cout << "--- Passing lvalue ---" << endl;
    auto w1 = createPerfect<Widget>(btnName);
    cout << "btnName after: '" << btnName << "'" << endl;  // unchanged

    cout << "\n--- Passing rvalue ---" << endl;
    auto w2 = createPerfect<Widget>(string("Icon"));

    cout << "\n--- Explicit move of lvalue ---" << endl;
    auto w3 = createPerfect<Widget>(move(btnName));
    cout << "btnName after move: '" << btnName << "'" << endl;

    cout << "\n--- Multiple args ---" << endl;
    auto w4 = createPerfect<Widget>(string("Panel"), 42);

    return 0;
}

Output:

Plaintext
--- Passing lvalue ---
Widget(const string&): 'Button' — copy
btnName after: 'Button'

--- Passing rvalue ---
Widget(string&&): 'Icon' — move

--- Explicit move of lvalue ---
Widget(string&&): 'Button' — move
btnName after move: ''

--- Multiple args ---
Widget(string, int): 'Panel' id=42

Step-by-step explanation:

  1. createPerfect<Widget>(btnName)btnName is an lvalue. Args is deduced as string&. forward<string&>(args) returns a string& (lvalue reference). Widget’s const string& constructor is chosen — the string is copied. btnName is unchanged.
  2. createPerfect<Widget>(string("Icon")) — the argument is a temporary rvalue. Args is deduced as string (no reference). forward<string>(args) returns a string&& (rvalue reference). Widget’s string&& constructor is chosen — the string is moved.
  3. createPerfect<Widget>(move(btnName))move(btnName) is an rvalue. Args deduced as string. Same as case 2: move constructor selected, btnName emptied.
  4. The single createPerfect template handles all combinations of lvalue and rvalue arguments, for any number of arguments, without writing multiple overloads.
  5. The ... in Args&&... args and forward<Args>(args)... handles variadic perfect forwarding — the same technique applied to a parameter pack. Each argument in the pack is forwarded individually with its own preserved value category.

std::forward vs. std::move: The Key Difference

std::move and std::forward are both casts, but they serve fundamentally different purposes:

C++
#include <iostream>
#include <string>
using namespace std;

void sink(string& s)  { cout << "sink(lvalue): " << s << endl; }
void sink(string&& s) { cout << "sink(rvalue): " << s << endl; }

// Using std::move: ALWAYS produces rvalue — even for lvalue inputs
template<typename T>
void badForward(T&& arg) {
    sink(move(arg));  // Wrong: always treats arg as rvalue, loses lvalue info
}

// Using std::forward: CONDITIONALLY preserves value category
template<typename T>
void goodForward(T&& arg) {
    sink(forward<T>(arg));  // Correct: lvalue→lvalue, rvalue→rvalue
}

int main() {
    string s = "hello";

    cout << "--- badForward ---" << endl;
    badForward(s);              // Passes lvalue — but move() ignores that!
    badForward(string("temp")); // Passes rvalue
    // After badForward(s): s might be empty (moved from!)
    cout << "s after badForward: '" << s << "'" << endl;

    s = "hello";  // Restore

    cout << "\n--- goodForward ---" << endl;
    goodForward(s);              // Preserves lvalue nature
    goodForward(string("temp")); // Preserves rvalue nature
    cout << "s after goodForward: '" << s << "'" << endl;  // unchanged!

    return 0;
}

Output:

Plaintext
--- badForward ---
sink(rvalue): hello
sink(rvalue): temp
s after badForward: ''

--- goodForward ---
sink(lvalue): hello
sink(rvalue): temp
s after goodForward: 'hello'

Step-by-step explanation:

  1. badForward uses std::move(arg), which always produces an rvalue. When s (an lvalue) is passed, move(arg) still converts it to an rvalue, causing sink(string&&) to be called and s to be moved from (emptied). This is incorrect — the caller passed an lvalue and expected it to be preserved.
  2. goodForward uses std::forward<T>(arg). When s (an lvalue) is passed, T = string&, and forward<string&>(arg) returns a string& (lvalue). sink(string&) is called — s is read but not consumed.
  3. When the temporary is passed, T = string, and forward<string>(arg) returns string&& (rvalue). sink(string&&) is called correctly.
  4. The rule: Use std::move when you want to unconditionally transfer resources from a named object. Use std::forward<T> exclusively inside template functions with universal references, when you want to preserve the caller’s value category.

Never use std::forward outside a forwarding reference context — it will not do what you expect.

Real-World Applications of Perfect Forwarding

Perfect forwarding appears throughout the C++ standard library and in well-designed generic code.

1. std::make_unique and std::make_shared

These factory functions must forward constructor arguments to new T(...) with perfect forwarding so that both copy and move constructors of T are correctly triggered.

C++
// Simplified implementation of make_unique
template<typename T, typename... Args>
unique_ptr<T> make_unique(Args&&... args) {
    return unique_ptr<T>(new T(forward<Args>(args)...));
}
C++
#include <iostream>
#include <memory>
#include <string>
using namespace std;

struct Config {
    string name;
    int    version;

    Config(string n, int v) : name(move(n)), version(v) {
        cout << "Config('" << name << "', " << version << ") constructed" << endl;
    }
    Config(const Config& other) : name(other.name), version(other.version) {
        cout << "Config COPIED" << endl;
    }
};

int main() {
    string cfgName = "Production";

    // make_unique forwards cfgName as lvalue, 3 as rvalue
    auto cfg1 = make_unique<Config>(cfgName, 3);
    cout << "cfgName still: '" << cfgName << "'" << endl;

    // make_unique forwards temporary as rvalue
    auto cfg2 = make_unique<Config>(string("Debug"), 1);

    return 0;
}

Output:

Plaintext
Config('Production', 3) constructed
cfgName still: 'Production'
Config('Debug', 1) constructed

cfgName is preserved because make_unique forwarded it as an lvalue to Config‘s constructor, which takes string n by value and copies it. The temporary string("Debug") was forwarded as an rvalue and moved.

2. std::vector::emplace_back

emplace_back constructs elements in-place by perfectly forwarding its arguments directly to the element’s constructor — no temporary object is created.

C++
#include <iostream>
#include <vector>
#include <string>
using namespace std;

struct Point {
    double x, y;
    string label;

    Point(double x, double y, string lbl)
        : x(x), y(y), label(move(lbl)) {
        cout << "Point(" << x << ", " << y << ", " << label << ") constructed" << endl;
    }
    Point(const Point& p) : x(p.x), y(p.y), label(p.label) {
        cout << "Point COPIED" << endl;
    }
    Point(Point&& p) noexcept : x(p.x), y(p.y), label(move(p.label)) {
        cout << "Point MOVED" << endl;
    }
};

int main() {
    vector<Point> points;
    points.reserve(3);

    string lbl = "Origin";

    cout << "--- push_back (constructs then moves) ---" << endl;
    points.push_back(Point(0.0, 0.0, "A"));  // Constructs temp, then moves into vector

    cout << "\n--- emplace_back (constructs in-place, no move) ---" << endl;
    points.emplace_back(1.0, 2.0, lbl);          // Forwards lbl as lvalue, constructs in-place
    points.emplace_back(3.0, 4.0, move(lbl));    // Forwards lbl as rvalue, constructs in-place

    cout << "\nlbl after emplace with move: '" << lbl << "'" << endl;

    return 0;
}

Output:

Plaintext
--- push_back (constructs then moves) ---
Point(0, 0, A) constructed
Point MOVED

--- emplace_back (constructs in-place, no move) ---
Point(1, 2, Origin) constructed
Point(3, 4, Origin) constructed

lbl after emplace with move: ''

Step-by-step explanation:

  1. push_back(Point(0.0, 0.0, "A")) first constructs a temporary Point, then moves it into the vector’s storage. Two operations: construct + move.
  2. emplace_back(1.0, 2.0, lbl) perfectly forwards the arguments directly to Point‘s constructor, which runs inside the vector’s storage. Only one operation: construct in-place. No temporary is created, no move is needed.
  3. emplace_back(3.0, 4.0, move(lbl)) forwards lbl as an rvalue. The constructor receives a string&& for its lbl parameter, which it then moves into the member. This is the most efficient path.
  4. The difference between push_back and emplace_back is precisely perfect forwarding: emplace_back uses Args&&... with forward<Args>(args)..., while push_back with a value argument constructs a temporary first.

3. Generic Wrapper Functions

Perfect forwarding is essential for any function that wraps another function call and needs to be transparent about performance.

C++
#include <iostream>
#include <string>
#include <chrono>
#include <functional>
using namespace std;

// A timing wrapper that measures any function call
// Perfect forwarding ensures no performance penalty from wrapping
template<typename Func, typename... Args>
auto timed(const string& label, Func&& f, Args&&... args)
    -> invoke_result_t<Func, Args...>
{
    auto start = chrono::high_resolution_clock::now();
    auto result = invoke(forward<Func>(f), forward<Args>(args)...);
    auto end = chrono::high_resolution_clock::now();

    auto us = chrono::duration_cast<chrono::microseconds>(end - start).count();
    cout << "[" << label << "] took " << us << " µs" << endl;

    return result;
}

string processString(string s) {
    // Simulate work
    for (char& c : s) c = toupper(c);
    return s;
}

int main() {
    string input = "hello world";

    // Forward lvalue — input is copied into processString's parameter
    auto r1 = timed("lvalue path", processString, input);
    cout << "r1: " << r1 << endl;
    cout << "input unchanged: " << input << endl;

    // Forward rvalue — input is moved into processString's parameter
    auto r2 = timed("rvalue path", processString, move(input));
    cout << "r2: " << r2 << endl;
    cout << "input after move: '" << input << "'" << endl;

    return 0;
}

Output:

Plaintext
[lvalue path] took 3 µs
r1: HELLO WORLD
input unchanged: hello world
[rvalue path] took 1 µs
r2: HELLO WORLD
input after move: ''

Step-by-step explanation:

  1. timed is a fully generic timing wrapper. Func&& is a universal reference to the function, and Args&&... is a variadic pack of universal references to the arguments.
  2. forward<Func>(f) and forward<Args>(args)... pass everything through with preserved value categories. The wrapper adds zero semantic overhead — calling timed(label, processString, x) is behaviorally identical to calling processString(x) with timing code around it.
  3. When input (lvalue) is passed in the first call, it is forwarded as an lvalue to processString, which takes its string s parameter by value and copies it. input is preserved.
  4. When move(input) is passed in the second call, it is forwarded as an rvalue, so processString‘s parameter is move-constructed from input. No copy of the string data occurs. input is emptied.

Forwarding with Lambdas (C++14 and Later)

C++14 introduced generic lambdas where parameter types can be auto, and C++20 introduces explicit template parameter lists for lambdas. Both enable perfect forwarding inside lambdas.

C++
#include <iostream>
#include <string>
using namespace std;

void process(string& s)  { cout << "process(lvalue): " << s << endl; }
void process(string&& s) { cout << "process(rvalue): " << s << endl; }

int main() {
    // C++14 generic lambda with perfect forwarding
    auto forwardingLambda = [](auto&& arg) {
        // auto&& is a universal reference in this context
        process(forward<decltype(arg)>(arg));
    };

    string s = "hello";

    forwardingLambda(s);              // lvalue → lvalue preserved
    forwardingLambda(string("world")); // rvalue → rvalue preserved
    forwardingLambda(move(s));         // explicit rvalue → rvalue preserved
    cout << "s after: '" << s << "'" << endl;

    return 0;
}

Output:

Plaintext
process(lvalue): hello
process(rvalue): world
process(rvalue): hello
s after: ''

Step-by-step explanation:

  1. auto&& arg in a generic lambda is a universal reference, just like T&& in a template function. It binds to both lvalues and rvalues.
  2. Inside a generic lambda, you cannot use T directly (there is no explicit T). Instead, use decltype(arg) to recover the deduced type: forward<decltype(arg)>(arg) is the idiomatic way to perfectly forward in a generic lambda.
  3. When s (lvalue string) is passed, decltype(arg) is string&, and forward<string&>(arg) returns a string& (lvalue). The lvalue process overload is chosen.
  4. When a temporary is passed, decltype(arg) is string&&, and forward<string&&>(arg) collapses to… wait — there is a subtlety: for rvalue reference types in forward, the cast becomes static_cast<string&& &&> which collapses to string&&. The rvalue overload is chosen.

Common Mistakes with Rvalue References and Perfect Forwarding

Mistake 1: Calling std::forward Multiple Times on the Same Argument

C++
template<typename T>
void badDouble(T&& arg) {
    doFirstThing(forward<T>(arg));   // Might move from arg
    doSecondThing(forward<T>(arg));  // Uses potentially moved-from arg!
}

Once you forward an argument as an rvalue (potentially moving it), the source is in an unspecified state. If you need to use the argument multiple times, only forward it on the last use, or copy it first:

C++
template<typename T>
void safeDouble(T&& arg) {
    doFirstThing(arg);               // Use as lvalue first time
    doSecondThing(forward<T>(arg));  // Forward (potentially move) last time
}

Mistake 2: Using std::forward Outside a Universal Reference Context

C++
void notATemplate(string&& s) {
    // s is a plain rvalue ref parameter — NOT a universal reference
    process(forward<string>(s));  // Works, but misleading — just write move(s)
}

std::forward is meaningful only inside template functions where the type is deduced. In a non-template context, forward<T> is equivalent to move, but using move is clearer.

Mistake 3: Mistaking auto&& for Always-Rvalue

C++
auto&& r1 = 42;        // Fine: rvalue ref to temporary int
int x = 10;
auto&& r2 = x;         // Also fine: r2 is int& (universal ref bound to lvalue)
// r2 is actually an lvalue reference here!

auto&& is a universal reference, not always an rvalue reference. When initialized with an lvalue, it becomes an lvalue reference via reference collapsing.

Mistake 4: Expecting Perfect Forwarding to Work Through Copies

C++
template<typename T>
void wrapper(T arg) {           // Takes by value — NOT a universal reference
    inner(forward<T>(arg));     // arg is always an lvalue here — forward does nothing useful
}

T arg (without &&) is a value parameter. The argument has already been copied or moved into arg. You cannot recover the original value category. For perfect forwarding, the parameter must be T&&.

Mistake 5: Not Using std::forward with Variadic Packs

C++
// WRONG: arg1... passed without forward — always lvalues
template<typename... Args>
void badVariadic(Args&&... args) {
    inner(args...);          // All forwarded as lvalues — loses rvalue info
}

// CORRECT: each arg forwarded with its own type
template<typename... Args>
void goodVariadic(Args&&... args) {
    inner(forward<Args>(args)...);  // Each arg preserves value category
}

Summary: The Complete Mental Model

Understanding rvalue references and perfect forwarding requires holding a few key ideas together:

T&& means two things. When T is deduced, it is a universal reference that binds to both lvalues and rvalues. When T is fully specified, it is a plain rvalue reference that only binds to rvalues.

Reference collapsing is the mechanism. When an lvalue is passed to a universal reference, T is deduced as T&, and T& && collapses to T&. When an rvalue is passed, T is deduced as plain T, and T && stays T&&.

Named rvalue references are lvalues. Inside a function body, even an T&& parameter is a named variable — an lvalue. This prevents accidental double-moves.

std::move is an unconditional rvalue cast. Use it when you knowingly want to transfer resources from a specific named object.

std::forward<T> is a conditional cast. Use it only inside templates with universal references, to preserve the caller’s value category when passing to inner functions.

Perfect forwarding enables single-template generic wrappers. Without it, every N-argument wrapper function needs 2^N overloads to handle all lvalue/rvalue combinations.

FeatureSyntaxBinds toUse when
Lvalue referenceT&lvalues onlyNon-owning, non-moving access
Const lvalue referenceconst T&lvalues + rvaluesRead-only access to anything
Rvalue referenceT&& (specified)rvalues onlyMove constructor/assignment
Universal referenceT&& (deduced)lvalues + rvaluesPerfect forwarding in templates
std::move(x)cast to T&&Explicitly grant move permission
std::forward<T>(x)conditional castPreserve value category in templates

Conclusion

Rvalue references and perfect forwarding are among the most powerful — and most carefully designed — features in C++11. Rvalue references gave the language a way to express “I have a temporary, and you may consume its resources,” enabling move semantics that eliminated an entire category of unnecessary heap allocations. Perfect forwarding extended this to generic code, giving template authors the ability to write single, efficient wrappers that behave exactly as if the wrapped function were called directly.

The seemingly paradoxical dual meaning of T&& — plain rvalue reference when the type is specified, universal reference when deduced — is resolved by understanding reference collapsing. The seemingly mysterious behavior of std::forward is resolved by understanding that it is simply static_cast<T&&>, relying on reference collapsing to do the right thing based on what T was deduced to be.

Mastering these concepts opens the door to writing truly efficient generic library code: factory functions that construct in-place, wrappers that add behavior without adding overhead, containers that store elements without unnecessary copies, and algorithms that run as fast as hand-written specialized code. These are the tools that make the C++ standard library itself both maximally expressive and maximally performant.

Share:
Subscribe
Notify of
0 Comments
Inline Feedbacks
View all comments

Discover More

Diodes: Operation and Applications

Explore diodes, their operation, types, and applications in electronics. Learn how these essential components work…

Understanding Voltage: The Driving Force of Electronics

Explore the critical role of voltage in electronics, from powering devices to enabling advanced applications…

China Pushes Domestic AI Chips into Government Procurement

China has taken a bold step toward technology self-reliance, placing locally produced AI chips from…

Getting Started with Python for Artificial Intelligence

Learn how to get started with Python for AI. Explore essential libraries, build models and…

Privacy Policy

Last updated: July 29, 2024 This Privacy Policy describes Our policies and procedures on the…

Introduction to Gradient Descent Optimization

Introduction to Gradient Descent Optimization

Learn gradient descent, the optimization algorithm that trains machine learning models. Understand batch, stochastic, and…

Click For More
0
Would love your thoughts, please comment.x
()
x