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):
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¶
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&¶meter to be an rvalue — it's an lvalue;std::moveit. std::moveon a forwarding reference — moves from the caller's lvalues. Usestd::forward.return std::move(local);— blocks NRVO.- Forwarding twice.
std::moveonconstobjects — silently copies.- Unconstrained forwarding constructors hijacking copy construction.
autoinstead ofdecltype(auto)in forwarding wrappers — returns copies.
Exercises¶
- For each expression, name its value category:
x,std::move(x),x + 1,v[0],std::string{"a"},f()wherefreturnsint&,g()wheregreturnsint&&. - Write
template <typename T> void show(T&& x)that prints whetherTwas deduced as an lvalue reference (std::is_lvalue_reference_v<T>). Call it with a variable, a literal, andstd::move(var). - Implement
my_make_uniqueand amy_emplace_backfor a simple vector-like class that constructs in place with placementnew(::new (ptr) T(std::forward<Args>(args)...)). - Reproduce the
Personforwarding-constructor hijack, read the error, then fix it two ways (a constraint and a by-value parameter). - Instrument a type's copy/move constructors and measure how many of each happen when filling a
std::vectorof 1,000 elements withpush_back(T{}),push_back(t),emplace_back(args), with and withoutreserve, and with and withoutnoexceptmoves. - Write a
Builderwith&/&&-qualifiedbuild()and confirm with a counter thatBuilder{}.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 deducedTis 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.