Effective Modern C++, Chapter 5

Rvalue References, Move Semantics, and Perfect Forwarding

std::move doesn't move. std::forward doesn't forward. At runtime, neither generates a single byte of code. They are casts, and understanding them is the key to writing fast modern C++.

Prerequisites: Chapters 1-4. Understanding of lvalues vs rvalues.
11
Chapters
4+
Simulations

Chapter 0: std::move and std::forward

Let's get the shocking truth out of the way: std::move doesn't move anything. std::forward doesn't forward anything. At runtime, they generate zero executable code. They are casts.

c++
// Simplified implementation of std::move
template<typename T>
decltype(auto) move(T&& param) {
    using ReturnType = std::remove_reference_t<T>&&;
    return static_cast<ReturnType>(param);
}

std::move unconditionally casts its argument to an rvalue. That's all it does. The cast enables moving by making the argument eligible for move constructors and move assignment operators, but the actual move happens (or doesn't) in the function that receives the rvalue reference.

std::move = unconditional cast to rvalue. It doesn't move. It makes the argument movable. Whether a move actually happens depends on whether the receiving type has move operations.

std::forward is a conditional cast: it casts to rvalue only if its argument was initialized with an rvalue. This is what enables perfect forwarding — passing arguments to another function while preserving their original value category:

c++
template<typename T>
void wrapper(T&& arg) {
    target(std::forward<T>(arg));
    // If arg was initialized with rvalue: forwards as rvalue
    // If arg was initialized with lvalue: forwards as lvalue
}
The rule: Use std::move on rvalue references (you know they're rvalues). Use std::forward on universal references (they might be either). Never use std::move on universal references — you might move from an lvalue the caller didn't expect to be modified.
What does std::move actually do at runtime?

Chapter 1: Universal References

Not every T&& is an rvalue reference. When type deduction is involved, T&& becomes a universal reference (also called a "forwarding reference") — it can bind to lvalues and rvalues:

c++
// Universal reference: T is deduced
template<typename T>
void f(T&& param);     // universal reference

// Rvalue reference: no type deduction on Widget
void f(Widget&& param);  // rvalue reference

// Universal reference: auto deduction
auto&& var = expr;       // universal reference
Two conditions for a universal reference: (1) The form must be exactly T&& (not const T&&, not vector<T>&&). (2) Type deduction must occur for T. If either condition is violated, you have a plain rvalue reference.
c++
// NOT universal references (no deduction on &&):
void f(const T&& param);       // rvalue ref (const)
void f(std::vector<T>&& p);  // rvalue ref (not T&&)

// ALSO NOT universal (push_back's T is already known):
template<class T>
class vector {
    void push_back(T&& x);  // rvalue ref, T fixed at class instantiation
};
Why is void f(const T&& param) NOT a universal reference?

Chapter 2: std::move on Rvalue Refs, std::forward on Universal Refs

The rule is simple: apply std::move to rvalue references (you know they bind to rvalues), and std::forward to universal references (they might be either):

c++
class Widget {
public:
    // rhs is rvalue ref: always use std::move
    Widget(Widget&& rhs) : name(std::move(rhs.name)) {}

    // newName is universal ref: use std::forward
    template<typename T>
    void setName(T&& newName) {
        name = std::forward<T>(newName);
    }
};
Never apply std::move to a universal reference. If the caller passed an lvalue, you'd move from it — modifying the caller's object unexpectedly. std::forward preserves the value category, moving only when the original argument was an rvalue.

For functions returning by value, apply std::move to rvalue references and std::forward to universal references being returned. But never apply std::move to local variables being returned — this prevents Return Value Optimization (RVO).

c++
// DO NOT do this: prevents RVO
Widget makeWidget() {
    Widget w;
    return std::move(w);  // BAD: inhibits RVO
}

// Just return the local: compiler applies RVO or implicit move
Widget makeWidget() {
    Widget w;
    return w;  // GOOD: RVO or implicit move
}
Why should you never std::move a local variable in a return statement?

Chapter 3: The Overloading Danger

Overloading a function that takes a universal reference is almost always a mistake. The universal reference overload is a greedy template that will match nearly anything:

c++
class Person {
public:
    template<typename T>
    explicit Person(T&& n) : name(std::forward<T>(n)) {}

    explicit Person(int idx);  // overload for index lookup
};

Person p("Nancy");     // OK: calls universal ref ctor
Person p(42);           // OK: calls int ctor

short idx = 42;
Person p(idx);            // ERROR: calls universal ref ctor!
                          // short matches T&& better than int
The problem: A universal reference template generates an exact-match overload for virtually any argument type. The int overload requires a conversion from short, so the template wins. It then tries to initialize std::string from a short — compile error.

It gets worse with copy/move constructors. If a non-const lvalue of type Person is passed, the universal reference template matches better than the copy constructor (which takes const Person&):

c++
Person p("Nancy");
auto cloneOfP(p);  // calls universal ref ctor, NOT copy ctor!
                   // Template deduces Person&, exact match.
                   // Copy ctor takes const Person& (requires adding const).
Why does a universal reference template beat the copy constructor for a non-const lvalue?

Chapter 4: Alternatives to Universal Ref Overloading

If you can't overload on universal references safely, what do you do? Meyers presents several alternatives:

Option 1: Abandon overloading
Give functions different names. logAndAddName(T&&) and logAndAddIdx(int).
Option 2: Pass by const T&
Sacrifices move optimization but avoids template greediness entirely.
Option 3: Pass by value
For copyable, cheap-to-move params (see Item 41). One extra move vs perfect forwarding.
Option 4: Tag dispatch
Dispatch internally based on type traits. The public API takes T&&, then calls an implementation function with a tag.
Option 5: enable_if (SFINAE)
Constrain the template so it doesn't match types you don't want. The most flexible but most complex approach.
c++
// Option 5: enable_if constrains the template
class Person {
public:
    template<typename T,
             typename = std::enable_if_t<
                 !std::is_base_of_v<Person, std::decay_t<T>>
                 && !std::is_integral_v<std::remove_reference_t<T>>
             >>
    explicit Person(T&& n) : name(std::forward<T>(n)) {}

    explicit Person(int idx);
};
What does std::enable_if do in the context of universal reference overloading?

Chapter 5: Reference Collapsing

C++ forbids references to references in source code, but the compiler creates them internally during template instantiation. The reference collapsing rules resolve them:

The rule is simple: If either reference is an lvalue reference, the result is an lvalue reference. Only rvalue-ref to rvalue-ref produces an rvalue reference.
Template Arg&& appliedCollapsed to
T = Widget&Widget& &&Widget& (lvalue ref)
T = Widget&&Widget&& &&Widget&& (rvalue ref)
T = WidgetWidget &&Widget&& (rvalue ref)

This is the mechanism that makes universal references and std::forward work. When an lvalue is passed to template<typename T> void f(T&&), T is deduced as Widget&. The parameter type becomes Widget& &&, which collapses to Widget& — an lvalue reference. When an rvalue is passed, T is Widget, and the parameter is Widget&&.

Reference Collapsing Visualized

See how lvalues and rvalues flow through a universal reference.

What does Widget& && collapse to?

Chapter 6: Don't Assume Move Is Always Better

Move semantics are C++11's marquee performance feature, but there are several situations where moving isn't present, isn't cheap, or isn't used:

SituationWhy move doesn't help
No move operationsMany C++98 legacy types don't have move ctors. "Moving" them falls back to copying.
Move not fasterstd::array moves are O(n) — same as copy. Small-string optimization (SSO) means short strings copy their data, not just a pointer.
Move can't be usedContexts requiring strong exception guarantees (e.g., vector reallocation) only use move if it's noexcept.
Source is lvalueOnly rvalues and xvalues trigger moves. Lvalues are copied unless you explicitly std::move them.
Key insight: In generic code (templates), you can't assume move operations exist or are cheap. Write code that's efficient for both movable and non-movable types. In concrete code where you know the types, you can rely on move semantics with confidence.
Why is moving a std::array not faster than copying it?

Chapter 7: Perfect Forwarding Failures

Perfect forwarding fails when template type deduction deduces the wrong type, or can't deduce at all. Here are the main failure cases:

Braced initializers
fwd({1, 2, 3}) fails. Templates can't deduce std::initializer_list from braces. Fix: use auto il = {1,2,3}; fwd(il);
0 or NULL as null pointers
Deduced as int, not pointer. Fix: use nullptr (see Item 8).
Declaration-only static const members
Taking a reference to a static const data member that's declared but not defined can fail to link. Fix: define it, or pass by value first.
Overloaded function names
fwd(processWidget) fails if processWidget is overloaded. The template can't know which overload. Fix: cast to the desired function pointer type.
Bitfields
Non-const references can't bind to bitfields. Fix: copy the bitfield value first, then forward the copy.
Why does fwd({1, 2, 3}) fail with perfect forwarding?

Chapter 8: Move Semantics Visualized

Watch how move semantics transfer resources instead of copying them. Click to see how a std::string is moved vs copied.

Copy vs Move: String Transfer

Click the buttons to see each operation animate.

Chapter 9: Beyond

ItemKey Takeaway
Item 23std::move = unconditional cast to rvalue. std::forward = conditional cast. Neither generates runtime code.
Item 24T&& with type deduction = universal reference. Otherwise = rvalue reference.
Item 25std::move on rvalue refs, std::forward on universal refs. Never std::move a return value.
Item 26Don't overload on universal references — they match almost everything.
Item 27Alternatives: different names, const T&, pass-by-value, tag dispatch, enable_if.
Item 28Reference collapsing: & + anything = &. Only && + && = &&.
Item 29Don't assume move is always present, cheap, or used.
Item 30Perfect forwarding fails for braces, 0/NULL, static const, overloaded names, bitfields.
Next: Chapter 6: Lambda Expressions covers the other half of modern C++'s power duo: closures that capture, move, and forward.

"std::move doesn't move anything. std::forward doesn't forward anything. At runtime, neither does anything at all." — Scott Meyers