Skip to content

1. Move Semantics & Perfect Forwarding

What & why

The intermediate track taught the rules: std::move casts, moved-from objects are valid-but-unspecified, mark moves noexcept. This lesson explains the model underneath — value categories and reference collapsing — so you can write generic code that preserves "copy-ness" or "move-ness" exactly, and read library signatures like template <class... Args> T& emplace_back(Args&&... args) with confidence.

Value categories

Every expression has a type and a value category. The categories that matter day to day:

Category Has identity? Can be moved from? Examples
lvalue yes not implicitly x, v[0], *p, s.name, a function returning T&
prvalue ("pure rvalue") no yes 42, a + b, T{}, a function returning T
xvalue ("expiring") yes yes std::move(x), a function returning T&&

rvalue = prvalue or xvalue. glvalue = lvalue or xvalue.

The key surprise: a named rvalue reference is an lvalue.

void take(std::string&& s) {
    // s has type std::string&&, but the EXPRESSION `s` is an lvalue (it has a name)
    std::string a = s;              // COPIES
    std::string b = std::move(s);   // moves
}

Reference binding rules

Parameter Binds to lvalues Binds to rvalues
T& yes no
const T& yes yes (extends temporary's lifetime)
T&& (concrete T) no yes

This is how overloading picks copy vs. move:

void push_back(const T& value);   // chosen for lvalues → copy
void push_back(T&& value);        // chosen for rvalues → move

Guaranteed copy elision and (N)RVO

Since C++17, initializing an object from a prvalue of the same type constructs it directly in place — no copy or move happens, and none needs to exist:

struct Pinned {
    Pinned() = default;
    Pinned(const Pinned&) = delete;
    Pinned(Pinned&&) = delete;
};

Pinned make() { return Pinned{}; }   // OK in C++17: guaranteed elision
Pinned p = make();                   // OK

Named return value optimization (returning a local variable) is not guaranteed but nearly universal; when it doesn't apply, the return is treated as an rvalue and moved automatically. That's why return std::move(local); is a pessimization: it turns a name into an xvalue and blocks NRVO.

std::vector<int> build() {
    std::vector<int> v(1000);
    return v;                 // NRVO or implicit move — never a copy
}

Forwarding references

In a template where T is deduced, T&& is not an rvalue reference — it's a forwarding reference (a.k.a. universal reference), and binds to anything:

template <typename T>
void f(T&& x);

int n = 0;
f(n);     // T = int&   → parameter type int& && → collapses to int&
f(42);    // T = int    → parameter type int&&

Reference collapsing

When references to references arise through templates or aliases:

Combination Result
& &, & &&, && & &
&& && &&

"Any lvalue reference wins."

Not forwarding references: std::vector<T>&& (not just T&&), const T&&, and auto&& is one only in a deduced context (it is, for local variables and range-for).

std::forward: perfect forwarding

Inside f, x is a named variable — always an lvalue. To pass it on with its original category, use std::forward<T>:

#include <memory>
#include <utility>

template <typename T, typename... Args>
std::unique_ptr<T> my_make_unique(Args&&... args) {
    return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}

std::forward<T>(x) is a conditional std::move: it casts to an rvalue only if T was deduced as a non-reference (i.e. the caller passed an rvalue).

Use When
std::move(x) x is an rvalue reference to a concrete type, or you're done with a local
std::forward<T>(x) x is a forwarding reference T&&

A wrapper that logs and forwards any call:

#include <iostream>
#include <utility>

template <typename F, typename... Args>
decltype(auto) logged(F&& f, Args&&... args) {
    std::cout << "calling with " << sizeof...(args) << " args\n";
    return std::forward<F>(f)(std::forward<Args>(args)...);
}

decltype(auto) preserves reference-ness of the returned value (plain auto would decay it to a copy).

Forward or move once

After forwarding/moving an object, don't use it again in the same function — the second use may see a moved-from shell:

template <typename T>
void bad(T&& x) {
    store_a(std::forward<T>(x));
    store_b(std::forward<T>(x));   // x may already be moved-from
}

Forwarding references vs. overloads

A forwarding-reference overload is greedy — it's often a better match than you expect:

class Person {
public:
    template <typename S>
    explicit Person(S&& name) : name_{std::forward<S>(name)} {}

    Person(const Person&) = default;
};

Person p{"Ada"};
Person q{p};   // calls the TEMPLATE (S = Person&, better match than const Person&) → error!

Constrain it (next lessons):

template <typename S>
    requires std::constructible_from<std::string, S>
explicit Person(S&& name);

Often the simpler fix is the by-value "sink" parameter from the intermediate track: explicit Person(std::string name) : name_{std::move(name)} {}.

emplace vs. push

emplace_back(args...) forwards args to the element's constructor, building it in place:

std::vector<std::pair<std::string, int>> v;
v.push_back({"Ada", 36});           // constructs a temporary pair, then moves it
v.emplace_back("Ada", 36);          // constructs the pair directly in the vector

Use emplace_back when constructing from arguments; push_back when you already have the object. Beware: emplace_back can call explicit constructors, so std::vector<std::unique_ptr<T>> v; v.emplace_back(raw_ptr); compiles where push_back wouldn't.

Move-only types and noexcept

static_assert(std::is_nothrow_move_constructible_v<MyType>);

Put this next to performance-critical types stored in vectors. If it fails, std::vector reallocation copies (or, for move-only types, moves with only the basic guarantee).

std::move_if_noexcept(x) is what containers use internally to decide.

Ref-qualified member functions

Overload on whether *this is an lvalue or rvalue:

class Builder {
public:
    std::string build() const& { return data_; }             // copy out of an lvalue
    std::string build() && { return std::move(data_); }      // steal from a temporary
private:
    std::string data_;
};

Builder b;
auto s1 = b.build();              // const& version
auto s2 = Builder{}.build();      // && version — no copy

C++23 "deducing this" (this auto&& self) collapses these overloads into one template.

Common mistakes

  • Expecting a named T&& parameter to be an rvalue — it's an lvalue; std::move it.
  • std::move on a forwarding reference — moves from the caller's lvalues. Use std::forward.
  • return std::move(local); — blocks NRVO.
  • Forwarding twice.
  • std::move on const objects — silently copies.
  • Unconstrained forwarding constructors hijacking copy construction.
  • auto instead of decltype(auto) in forwarding wrappers — returns copies.

Exercises

  1. For each expression, name its value category: x, std::move(x), x + 1, v[0], std::string{"a"}, f() where f returns int&, g() where g returns int&&.
  2. Write template <typename T> void show(T&& x) that prints whether T was deduced as an lvalue reference (std::is_lvalue_reference_v<T>). Call it with a variable, a literal, and std::move(var).
  3. Implement my_make_unique and a my_emplace_back for a simple vector-like class that constructs in place with placement new (::new (ptr) T(std::forward<Args>(args)...)).
  4. Reproduce the Person forwarding-constructor hijack, read the error, then fix it two ways (a constraint and a by-value parameter).
  5. Instrument a type's copy/move constructors and measure how many of each happen when filling a std::vector of 1,000 elements with push_back(T{}), push_back(t), emplace_back(args), with and without reserve, and with and without noexcept moves.
  6. Write a Builder with &/&&-qualified build() and confirm with a counter that Builder{}.build() doesn't copy.

Recap

  • Value categories: lvalues have identity; prvalues are temporaries; xvalues are expiring (std::move(x)).
  • A named rvalue reference is an lvalue.
  • C++17 guarantees elision for prvalues; returning a local moves or elides automatically.
  • T&& with deduced T is a forwarding reference; reference collapsing makes it bind to anything.
  • std::forward<T> preserves category; use it exactly once per forwarding reference.
  • Constrain greedy forwarding templates; mark moves noexcept.

Next: Variadic & advanced templates →