Move Semantics Advanced

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.

Learn Python, JavaScript, Java and more with free interactive lessons, real projects and AI-powered help. Beginner-friendly.

Part of the free C++ course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.

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 parameter — template <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.

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.

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.

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.

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.

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.

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.

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.

Practice quiz

When is T&& a forwarding (universal) reference rather than a plain rvalue reference?

  • Always — T&& is always a forwarding reference
  • When T is a concrete type like std::string
  • When T is a template parameter being deduced (or auto&&)
  • Only inside a class member function

Answer: When T is a template parameter being deduced (or auto&&). T&& is a forwarding reference only when T is a deduced template parameter (or auto&&); string&& is a plain rvalue reference.

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

  • std::move always casts to an rvalue; std::forward casts to rvalue only if the original was an rvalue
  • std::move conditionally casts; std::forward always casts to rvalue
  • They are identical
  • std::forward physically moves the data; std::move only marks it

Answer: std::move always casts to an rvalue; std::forward casts to rvalue only if the original was an rvalue. std::move is an unconditional rvalue cast; std::forward<T> preserves the caller's value category.

Inside void f(std::string&& s), the named parameter s is what?

  • An rvalue, so push_back(s) moves automatically
  • A forwarding reference
  • A const reference
  • An lvalue, so you must std::move(s) to actually move

Answer: An lvalue, so you must std::move(s) to actually move. A named rvalue reference is itself an lvalue; you must re-cast with std::move(s) or it copies.

In a perfect-forwarding factory makeWidget(Args&&... args), passing a named lvalue selects which constructor?

  • The move constructor
  • The copy constructor
  • Neither — it fails to compile
  • The default constructor

Answer: The copy constructor. std::forward preserves the lvalue category, so the copy constructor runs; an rvalue argument would select the move constructor.

Reference collapsing rule: which combination stays &&?

  • && &&
  • & &
  • & &&
  • && &

Answer: && &&. Any combination containing an lvalue reference (&) collapses to &; only && && stays &&.

When you pass an lvalue to a forwarding reference T&&, what does T deduce to?

  • U (giving U&&)
  • const U
  • U& (which collapses to a plain lvalue reference U&)
  • void

Answer: U& (which collapses to a plain lvalue reference U&). An lvalue makes T deduce to U&, yielding U& && that collapses to U&; that asymmetry is how forwarding 'remembers' the category.

For returning a local variable, why is return std::move(local); an anti-pattern?

  • It always fails to compile
  • It blocks (N)RVO / guaranteed copy elision, so it is at best equal, often slower
  • It leaks memory
  • It makes the move throw

Answer: It blocks (N)RVO / guaranteed copy elision, so it is at best equal, often slower. A plain return local; lets the compiler elide the copy/move entirely; wrapping in std::move prevents that elision.

Why does marking a move constructor noexcept matter for std::vector?

  • It makes the type smaller
  • It is required for the class to compile
  • It disables exceptions program-wide
  • It lets std::vector move (not copy) elements during reallocation

Answer: It lets std::vector move (not copy) elements during reallocation. On reallocation, vector only moves elements if the move ctor is noexcept; otherwise it copies to keep the strong guarantee.

What does std::move_if_noexcept(x) return?

  • Always an rvalue reference
  • An rvalue only when the move can't throw, otherwise an lvalue (so a copy is made)
  • Always an lvalue
  • A null pointer if x is empty

Answer: An rvalue only when the move can't throw, otherwise an lvalue (so a copy is made). It hands you an rvalue only when moving is noexcept; otherwise an lvalue so a safe copy is chosen.

On a forwarding reference inside a template, you should pass the argument onward with:

  • std::move(x)
  • a plain copy x
  • std::forward<T>(x)
  • &x

Answer: std::forward<T>(x). Use std::forward<T> on a deduced T&&; using std::move there would steal from an lvalue the caller still needs.