Skip to main content
    Courses/C++/Move Semantics Advanced

    Lesson 20 • Advanced

    Move Semantics, Perfect Forwarding & Rvalue References

    By the end of this lesson you'll be able to write generic code that forwards arguments without a single needless copy — choosing std::move vs std::forward correctly, reading reference-collapsing rules, and relying on copy elision and noexcept moves to make modern C++ fast.

    What You'll Learn

    • Tell a forwarding reference (T&& with deduced T) apart from a plain rvalue reference
    • Use std::forward to preserve a caller's lvalue/rvalue category through a wrapper
    • Apply the reference-collapsing rules that make perfect forwarding work
    • Choose std::move vs std::forward correctly — and never confuse them
    • Rely on RVO / guaranteed copy elision instead of forcing moves on return
    • Mark moves noexcept and use std::move_if_noexcept so containers stay fast

    💡 Real-World Analogy

    Think of a courier relay. A package arrives that is either a keepsake the owner still wants back (an lvalue — you must photocopy it and forward the copy) or a disposable parcel nobody will reclaim (an rvalue — you can just hand the original onward). Perfect forwarding is a courier who looks at a label on each package and passes it to the next stop in exactly the same condition it arrived — copy-it-onward for keepsakes, hand-it-onward for disposables. std::forward is that label-reader. std::move, by contrast, is a courier who relabels everything as disposable — useful, but only when you truly own the parcel.

    1. Forwarding references vs rvalue references

    You already know std::string&& is an rvalue reference: it binds only to temporaries. But the moment && is attached to a deduced template parametertemplate <typename T> void f(T&& x) or auto&& — it becomes something different: a forwarding reference (also called a universal reference). A forwarding reference binds to both lvalues and rvalues, and remembers which one it caught. That memory is what makes perfect forwarding possible.

    Written asWhat it isBinds to
    string&& rPlain rvalue reference (concrete type)rvalues only
    T&& x (T deduced)Forwarding referencelvalues and rvalues
    auto&& xForwarding referencelvalues and rvalues
    vector<T>&& xRvalue reference (T not at top level)rvalues only

    Read this worked example first. It is a perfect-forwarding factory: one template function that copies when handed a keepsake and moves when handed a disposable. Run it and watch which constructor fires for each call.

    Worked example: a perfect-forwarding factory

    One template, two outcomes — copy for lvalues, move for rvalues. Read every comment, then run it.

    Try it Yourself »
    C++
    #include <iostream>
    #include <string>
    #include <utility>   // std::forward, std::move
    using namespace std;
    
    struct Widget {
        string name;
        // Two constructors so we can SEE which one runs:
        Widget(const string& n) : name(n) {            // copies from an lvalue
            cout << "  copy-ctor   ('" << name << "')\n";
        }
        Widget(string&& n) : name(std::move(n)) {       // steals from an rvalue
            cout << "  move-ctor   ('" << name << "')\n";
        }
    };
    
    // A FORWARDING reference: T
    ...

    2. std::forward and perfect forwarding

    A forwarding reference parameter has a name, so inside the function it is an lvalue — even if it caught an rvalue. If you pass it straight on, everything becomes a copy. std::forward<T>(x) fixes that: it re-casts x back to its original category. It returns an rvalue only if the caller passed an rvalue; otherwise it leaves an lvalue alone. That conditional cast is the entire difference from std::move, which casts unconditionally.

    Your turn. The wrapper below catches its argument with a forwarding reference but passes it on incorrectly. Fill in the blank so the original category is preserved.

    🎯 Your turn: preserve the value category

    Fill in the ___ so lvalues copy and rvalues move. Check against the expected output.

    Try it Yourself »
    C++
    #include <iostream>
    #include <string>
    #include <utility>
    using namespace std;
    
    struct Logger {
        Logger(const string& s) { cout << "copied: "  << s << "\n"; }
        Logger(string&& s)      { cout << "moved:  "  << s << "\n"; }
    };
    
    // 🎯 YOUR TURN — finish this perfect-forwarding wrapper.
    template <typename T>
    Logger build(T&& value) {            // T&& is a FORWARDING reference here
        // We must preserve value's category. Inside this function 'value' is a
        // NAMED variable, so it is an lva
    ...

    3. When to use std::move vs std::forward

    The rule is short: use std::move on a plain rvalue reference (string&&), and std::forward<T> on a forwarding reference (deduced T&&). The trap is that a named rvalue reference is itself an lvalue, so if you forget the std::move, you silently copy. The exercise below has exactly that bug waiting — sink the string into the vector without copying it.

    🎯 Your turn: move, don't copy

    Re-cast the named rvalue reference so push_back moves it. Check the expected output.

    Try it Yourself »
    C++
    #include <iostream>
    #include <string>
    #include <vector>
    #include <utility>
    using namespace std;
    
    // 🎯 YOUR TURN — sink the string into the vector WITHOUT copying it.
    void store(vector<string>& out, string&& s) {
        // 's' is an rvalue REFERENCE, but because it now has a name it is an
        // lvalue inside this function. push_back will COPY unless you re-cast it.
        out.push_back(___);              // 👉 cast s back to an rvalue so it MOVES
        //                                  hint: std::mov
    ...

    4. Reference collapsing — why forwarding works

    You can't write a reference to a reference yourself, but template deduction can produce one internally. C++ then collapses it with one rule of thumb: if any & is involved, the result is &; only && && stays &&. So when you pass an lvalue to T&&, T deduces to U&, giving U& && which collapses to a plain lvalue reference. Pass an rvalue and T deduces to U, giving U&&. That asymmetry is precisely how a forwarding reference "remembers" what it caught.

    You writeDeduction givesCollapses to
    pass lvalue to T&&U& &&U& (lvalue ref)
    pass rvalue to T&&U &&U&& (rvalue ref)
    & & / & && / && &&
    && &&&&

    This example proves the rule at runtime using is_lvalue_reference. The same inspect template deduces a different T depending on what you hand it.

    Worked example: reference collapsing in action

    Watch T deduce to an lvalue ref or an rvalue ref depending on the argument.

    Try it Yourself »
    C++
    #include <iostream>
    #include <string>
    #include <type_traits>
    #include <utility>
    using namespace std;
    
    // A forwarding reference. What T deduces to drives reference collapsing:
    //   pass an lvalue  -> T = U&   -> U& && collapses to U&   (lvalue ref)
    //   pass an rvalue  -> T = U    -> U  && stays      U&&    (rvalue ref)
    template <typename T>
    void inspect(T&& x) {
        cout << "  is_lvalue_reference<T>: " << boolalpha
             << is_lvalue_reference<T>::value << "\n";
    }
    
    int main() {
        cout << "
    ...

    5. Copy elision (RVO/NRVO) & noexcept moves

    Copy elision is the compiler skipping a copy or move entirely by constructing the result straight into its destination. Since C++17, returning a temporary (a prvalue) is guaranteed to be elided — so return Heavy("x"); builds the object in the caller with zero moves. That's why return std::move(local); is an anti-pattern: it turns the value into something that can't be elided, blocking the optimisation it was meant to help.

    Separately, marking your move constructor noexcept is what lets std::vector move its elements during reallocation instead of copying them. std::move_if_noexcept encodes the rule directly: it hands you an rvalue only when the move can't throw, otherwise an lvalue so a safe copy is made.

    Worked example: elision, noexcept move & move_if_noexcept

    See the elided return, the move on vector growth, and move_if_noexcept choosing a move.

    Try it Yourself »
    C++
    #include <iostream>
    #include <vector>
    #include <string>
    #include <utility>   // std::move_if_noexcept
    using namespace std;
    
    struct Heavy {
        string label;
        Heavy(string l) : label(std::move(l)) { cout << "  ctor  " << label << "\n"; }
        Heavy(const Heavy& o) : label(o.label) { cout << "  COPY  " << label << "\n"; }
        // A noexcept move ctor lets std::vector move (not copy) on reallocation:
        Heavy(Heavy&& o) noexcept : label(std::move(o.label)) { cout << "  MOVE  " << label << "\n"; }
    ...

    Common Errors (and the fix)

    • Using std::move instead of std::forward in a template: writing return Inner(std::move(x)); on a forwarding reference forces a move even when the caller passed an lvalue they still need — you've stolen from their object. On a forwarding reference always use std::forward<T>(x).
    • Double-move: forwarding or moving the same variable twice — e.g. g(std::move(x)); h(std::move(x)); — leaves the second call reading a moved-from object. After a move, treat the source as empty; move each value exactly once.
    • Forwarding a named rvalue reference without std::move: inside void f(string&& s), s is an lvalue, so vec.push_back(s) copies. Write vec.push_back(std::move(s)) to actually move.
    • return std::move(local); — this disables (N)RVO and is usually slower, not faster. Just return local; and let the compiler elide.
    • Forgetting noexcept on the move constructor: without it, std::vector reallocation falls back to copying every element. Mark the move ctor noexcept when it genuinely can't throw.

    📋 Quick Reference

    GoalCodeNotes
    Forwarding referencetemplate<class T> f(T&& x)T deduced ⇒ binds both
    Perfect forwardstd::forward<T>(x)conditional cast
    Unconditional caststd::move(x)always an rvalue
    On a T&& (deduced)std::forward<T>preserve category
    On a string&&std::movenamed ⇒ is an lvalue
    Return a localreturn local;(N)RVO elides it
    Fast container growthT(T&&) noexceptenables move-on-realloc
    Safe conditional movestd::move_if_noexcept(x)move only if non-throwing

    Frequently Asked Questions

    Q: What is the difference between std::move and std::forward?

    std::move always casts to an rvalue — use it when you definitely want to steal from something, like an rvalue reference parameter. std::forward conditionally casts: it produces an rvalue only if the original argument was an rvalue, otherwise it leaves it as an lvalue. Use std::forward only on a forwarding reference (T&& with deduced T) to preserve the caller's value category.

    Q: Is T&& always an rvalue reference?

    No. T&& is an rvalue reference only when T is a concrete type, like std::string&&. When T is a template parameter being deduced (template <typename T> void f(T&& x)) or auto&&, T&& is a forwarding reference (also called a universal reference) and can bind to both lvalues and rvalues.

    Q: Why is a named rvalue reference an lvalue inside the function?

    Once an rvalue reference parameter has a name, you can take its address and refer to it repeatedly, so it behaves as an lvalue. That is intentional and safe — it stops you from accidentally moving from it on every use. To actually move out of it you must explicitly write std::move(param).

    Q: Should I write return std::move(x) to make returns faster?

    Usually no. For a local variable, return x; lets the compiler apply (N)RVO and construct the result directly in the caller, eliding the move entirely. Wrapping it in std::move blocks that elision and is at best the same, often slower. Only use std::move on return when returning a member or a function parameter, which are not eligible for NRVO.

    Q: Why does noexcept matter for a move constructor?

    When a std::vector reallocates, it must move or copy its existing elements. If your move constructor is noexcept, the vector moves them (fast). If it can throw, the vector copies instead (slow) to keep the strong exception guarantee. std::move_if_noexcept encodes this rule: it returns an rvalue only when moving cannot throw, otherwise an lvalue so a copy is made.

    Q: What is reference collapsing?

    When references-to-references appear during template deduction, C++ collapses them with one rule: any combination containing an lvalue reference collapses to an lvalue reference (& wins), and only && && collapses to &&. This is exactly what makes a forwarding reference work — deducing T as U& yields U& && which collapses to U&, while deducing T as U yields U&&.

    Mini-Challenge: your own forwarding factory

    No blanks this time — just a brief and an outline. Write the template yourself, call it three ways, and check your output against the comments. This is the exact pattern behind make_unique, emplace_back, and every modern factory.

    🎯 Mini-Challenge: build a forwarding factory

    Write make() with std::forward and prove it copies lvalues but moves rvalues.

    Try it Yourself »
    C++
    #include <iostream>
    #include <string>
    #include <utility>
    using namespace std;
    
    struct Resource {
        Resource(const string& s) { cout << "copy '" << s << "'\n"; }
        Resource(string&& s)      { cout << "move '" << s << "'\n"; }
    };
    
    int main() {
        // 🎯 MINI-CHALLENGE: a perfect-forwarding factory of your own
        // 1. Write:  template <typename T> Resource make(T&& v)
        //    that forwards v into a Resource using std::forward<T>(v).
        // 2. In main, call make() three times:
        //      - 
    ...

    Pro Tips

    • 💡 One memorised rule: std::move on string&&, std::forward<T> on deduced T&&. Never the other way round.
    • 💡 Forward exactly once: a forwarded argument may have been moved from, so use it a single time.
    • 💡 Trust the compiler on returns: in C++17 a returned prvalue is guaranteed to be elided — write return local;, not return std::move(local);.
    • 💡 Always noexcept your moves when they can't throw, or std::vector quietly copies on every reallocation.

    🎉 Lesson Complete

    • T&& with deduced T is a forwarding reference; string&& is a plain rvalue reference
    • std::forward<T> preserves the caller's category; std::move casts unconditionally
    • ✅ Reference collapsing: any & wins — only && && stays &&
    • ✅ A named rvalue reference is an lvalue — re-cast with std::move to actually move
    • ✅ Return locals plainly; (N)RVO and guaranteed elision beat return std::move(...)
    • noexcept moves + std::move_if_noexcept keep containers fast and safe
    • Next lesson: RAII Architecture — applying resource ownership patterns across complex systems

    Sign up for free to track which lessons you've completed and get learning reminders.

    Previous

    Cookie & Privacy Settings

    We use cookies to improve your experience, analyze traffic, and show personalized ads. You can manage your preferences below.

    By clicking "Accept All", you consent to our use of cookies for analytics and personalized advertising. You can customize your preferences or reject non-essential cookies.

    Privacy PolicyTerms of Service