Move Semantics — Advanced
Universal references, std::forward, reference collapsing, RVO/NRVO, Rule of Five, noexcept contracts, and the move pitfalls every C++ engineer must know.
Move Semanticssince C++11Move semantics enables transferring resource ownership from one object to another in O(1) by exploiting rvalue references — the compiler selects move constructors and move assignment operators instead of copying when the source is known to be expendable.
Overview
C++11 reworked the expression taxonomy from a simple lvalue/rvalue split into a two-axis system: has identity (the address can be taken) and can be moved from (resources may be stolen). Every expression falls into exactly one of five categories:
| Category | Has identity | Movable | Typical examples |
|---|---|---|---|
| lvalue | yes | no | named variable, *ptr, vec[i] |
| prvalue | no | yes | literal 42, T{}, returned temporary |
| xvalue | yes | yes | std::move(x), std::forward<T&&>(x) |
| glvalue | yes | — | lvalue ∪ xvalue |
| rvalue | — | yes | prvalue ∪ xvalue |
std::move casts a named lvalue into an xvalue — same object, now treated as movable. It performs no transfer by itself.
std::string s = "hello";
std::string t = std::move(s); // xvalue; move constructor runs
// s is valid-but-unspecified — do not read without re-assigningForwarding References and Reference Collapsing
A forwarding reference arises only when T&& appears in a context where T is a template parameter being deduced at that call site. The same syntax in a non-deduction context is an ordinary rvalue reference:
template<typename T>
void relay(T&& arg); // forwarding reference — T is deduced
void relay(int&& arg); // rvalue reference — int is fixed, no deduction
auto f = [](auto&& x); // forwarding reference — auto deduced per call (C++14)The underlying mechanism is reference collapsing (C++11): when a reference type is substituted for T, overlapping &/&& qualifiers collapse by a single rule — any combination containing at least one & yields &, while && && yields &&.
| T deduced as | T&& collapses to |
|---|---|
int (rvalue) | int&& |
int& (lvalue) | int& && → int& |
int&& (xvalue) | int&& && → int&& |
std::forward — conditional cast
std::forward<T> casts its argument to T&&, which then collapses according to what T was deduced as. When called with an lvalue argument, T is U&, so the cast yields U&. When called with an rvalue argument, T is U, so the cast yields U&&. Never use std::forward outside a forwarding-reference context.
// Canonical perfect-forwarding factory — the pattern behind std::make_unique (C++14)
template<typename T, typename... Args>
std::unique_ptr<T> make(Args&&... args) {
return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}
struct Widget { Widget(int n, std::string label); };
std::string name = "alpha";
auto w = make<Widget>(42, std::string("tmp")); // 42 as int&&, "tmp" as string&&
auto w2 = make<Widget>(7, name); // name as string& — no copy elision, original untouchedRule of Five (and Rule of Zero)
Declaring any of the five special member functions causes the compiler to suppress automatic generation of others in ways that are subtly dangerous. The rule: define all five, or none.
class Buffer {
std::byte* data_;
std::size_t size_;
public:
explicit Buffer(std::size_t n)
: data_(new std::byte[n]), size_(n) {}
~Buffer() { delete[] data_; }
// Copy — deep
Buffer(const Buffer& o)
: data_(new std::byte[o.size_]), size_(o.size_) {
std::memcpy(data_, o.data_, size_);
}
Buffer& operator=(const Buffer& o) {
if (this != &o) {
delete[] data_;
data_ = new std::byte[o.size_];
size_ = o.size_;
std::memcpy(data_, o.data_, size_);
}
return *this;
}
// Move — O(1); noexcept is non-negotiable (see below)
Buffer(Buffer&& o) noexcept
: data_(std::exchange(o.data_, nullptr)) // C++14
, size_(std::exchange(o.size_, 0)) {}
Buffer& operator=(Buffer&& o) noexcept {
if (this != &o) {
delete[] data_;
data_ = std::exchange(o.data_, nullptr);
size_ = std::exchange(o.size_, 0);
}
return *this;
}
};std::exchange(o.data_, nullptr) (C++14) atomically returns the old value and installs the new one — the idiomatic way to null-out moved-from pointer members without a separate assignment.
Rule of Zero — prefer this
Compose from RAII types and let the compiler generate all five correctly:
class Document {
std::unique_ptr<Schema> schema_; // move-only: no copy, free move
std::vector<Row> rows_; // movable and copyable
std::string title_; // movable and copyable
// Compiler generates: destructor, move ctor, move assign, copy ctor, copy assign
};noexcept Move Operations and std::vector
std::vector::push_back, resize, and reserve must uphold the strong exception guarantee: if reallocation throws, the original vector is unchanged. To guarantee this while moving elements to new storage, the library checks std::is_nothrow_move_constructible<T> at compile time:
- Move constructor is
noexcept→ elements are moved during reallocation (O(1) per element). - Move constructor may throw → elements are copied during reallocation (O(n) per reallocation).
class Cheap {
public:
Cheap(Cheap&&) noexcept = default; // vector will move — fast reallocation
};
class Expensive {
public:
Expensive(Expensive&&) {} // no noexcept — vector copies on every reallocation
};
// C++17 variable template
static_assert( std::is_nothrow_move_constructible_v<Cheap>);
static_assert(!std::is_nothrow_move_constructible_v<Expensive>);
// Propagate the guarantee in generic containers
template<typename T>
class Pool {
void reallocate(std::size_t n)
noexcept(std::is_nothrow_move_constructible_v<T>)
{
T* next = static_cast<T*>(::operator new(n * sizeof(T)));
for (std::size_t i = 0; i < size_; ++i)
new(next + i) T(std::move(buf_[i]));
// ...
}
};Mark move constructors and move assignment operators noexcept unless the implementation genuinely allocates. An unqualified move operation is a latent performance trap.
Return Value Optimization
The compiler eliminates copies and moves when constructing return values. Since C++17, this elision is mandatory for prvalues:
// NRVO (Named RVO) — permitted since C++11, applied by all major compilers
std::vector<int> load(int n) {
std::vector<int> v;
v.reserve(n);
for (int i = 0; i < n; ++i) v.push_back(i);
return v; // v constructed in-place in caller's storage — zero copies, zero moves
}
// Mandatory copy elision — guaranteed since C++17
std::vector<int> dest = std::vector<int>(1000); // no move, no copy
// Implicit move on return (C++11) — when NRVO does not apply,
// returning a local variable of the function's return type is treated as std::move
std::string pick(bool flag) {
std::string a = "alpha", b = "beta";
if (flag) return a; // NRVO candidate; move otherwise
return b; // same
}Since C++20, implicit move also applies when returning a by-value function parameter or an rvalue reference parameter — previously these required an explicit std::move.
Never write return std::move(local_var) for a local variable whose type matches the return type. It disables NRVO and forces a move where the compiler would have done zero work.
Forwarding in Lambdas
C++14 generic lambdas use auto&& as forwarding references, but calling std::forward requires naming the type — which is not in scope. C++20 template lambdas solve this cleanly:
// C++14 — decltype workaround
auto wrap14 = [](auto&& x) {
sink(std::forward<decltype(x)>(x));
};
// C++20 — explicit template parameter, readable and symmetric with function templates
auto wrap20 = []<typename T>(T&& x) {
sink(std::forward<T>(x));
};
// C++20 variadic — wrapping emplace-style APIs
auto emplacer = []<typename... Ts>(Ts&&... args) {
container.emplace_back(std::forward<Ts>(args)...);
};Common Pitfalls
Moving a const object produces a silent copy
const std::vector<int> src = {1, 2, 3};
std::vector<int> dst = std::move(src); // casts to const vector&&
// No move constructor accepts const&&; copy constructor (const&) wins instead
// Compiler emits no diagnostic by default — you pay O(n) expecting O(1)std::move in a return statement suppresses NRVO
std::string build() {
std::string result;
// ...
return std::move(result); // WRONG — prevents NRVO, forces a move
return result; // CORRECT — NRVO applies; implicit move as fallback
}Partially consuming a container in a range-for
std::vector<std::string> words{"alpha", "beta", "gamma"};
for (auto& w : words)
process(std::move(w)); // each element moved-from; words.size() still 3, all hollow
// To consume the whole container element-by-element:
for (auto w : std::move(words)) // moves vector into range; iterates by value
process(std::move(w));
// words is now emptyForgetting to null out the moved-from pointer
// Naive move constructor — double-free waiting to happen
MyResource(MyResource&& o) noexcept {
data_ = o.data_;
// o.data_ still points at the buffer; o's destructor will delete[] it
}
// Correct — use std::exchange (C++14)
MyResource(MyResource&& o) noexcept
: data_(std::exchange(o.data_, nullptr)) {}Best Practices
- Prefer Rule of Zero: compose from
unique_ptr,vector,string; get correct specials for free. - When you must write a move constructor, always mark it
noexceptunless it genuinely allocates. - Use
std::exchange(C++14) to null-out moved-from pointer members in a single expression. - Return local variables without
std::move— trust NRVO and implicit move. - Never read a moved-from object without first assigning a new value to it.
- In generic code, propagate
noexceptwithnoexcept(std::is_nothrow_move_constructible_v<T>).
std::move_if_noexcept
std::move_if_noexcept is the standard library's own mechanism for conditional moving under the strong exception guarantee. It returns an rvalue reference to its argument only when the move constructor is noexcept; otherwise it returns a const lvalue reference, triggering the copy constructor instead. This is exactly what std::vector uses internally during reallocation.
// C++11
#include <utility>
struct Safe { Safe(Safe&&) noexcept; }; // noexcept move — will be moved
struct Unsafe { Unsafe(Unsafe&&); }; // throwing move — will be copied
Safe s;
Unsafe u;
auto&& rs = std::move_if_noexcept(s); // Safe&& — move proceeds
auto&& ru = std::move_if_noexcept(u); // const Unsafe& — copy proceeds
// Equivalent conditional expression (illustrative)
// std::is_nothrow_move_constructible_v<T> ? std::move(x) : xUse std::move_if_noexcept in generic containers and algorithms where the strong exception guarantee must be preserved but you still want to take advantage of moves when they are safe.
Moved-from State and Reuse
The standard guarantees only that a moved-from object is in a valid but unspecified state — not that it is empty, zero, or null. The object's invariants hold (it can be destructed and assigned to), but you must not make assumptions about its value without first observing or reassigning it.
// C++11
std::vector<int> v = {1, 2, 3, 4};
std::vector<int> w = std::move(v);
// v is valid-but-unspecified: size() may be 0 or some implementation detail
// Safe: re-assign before use
v = {10, 20, 30}; // v is fully defined again
// Safe: check before using
if (!v.empty()) { /* ... */ }
// Safe: call operations with no preconditions (clear, assign, push_back)
v.clear();
v.push_back(99);
// Unsafe: assume moved-from string is empty (it usually is, but the standard
// does NOT require it for all implementations)
std::string s = "hello";
std::string t = std::move(s);
assert(s.empty()); // passes on libc++/libstdc++ but is not guaranteed by the standardThe exception is types that document a stronger post-condition: std::unique_ptr guarantees nullptr after a move, and std::shared_ptr guarantees nullptr and use_count() == 0.
// C++11 — move-from guarantees documented by the standard
std::unique_ptr<int> p = std::make_unique<int>(42);
std::unique_ptr<int> q = std::move(p);
assert(p == nullptr); // guaranteed by [unique.ptr.single.ctor]Move Semantics with Inheritance
A derived class move constructor must explicitly forward to the base class move constructor; otherwise the base subobject is copy-constructed. The compiler-generated move constructor does this correctly only when no user-declared special members exist in the class hierarchy.
// C++11
struct Base {
std::string data_;
Base(Base&& o) noexcept : data_(std::move(o.data_)) {}
Base& operator=(Base&& o) noexcept {
data_ = std::move(o.data_);
return *this;
}
};
struct Derived : Base {
std::vector<int> extra_;
// Correct: base subobject is moved via Base(Base&&)
Derived(Derived&& o) noexcept
: Base(std::move(o)) // explicit cast required
, extra_(std::move(o.extra_)) {}
Derived& operator=(Derived&& o) noexcept {
Base::operator=(std::move(o)); // explicit base move-assign
extra_ = std::move(o.extra_);
return *this;
}
};Forgetting Base(std::move(o)) and writing Base(o) instead silently invokes the base copy constructor — the derived object moves, the base part copies. This is a common silent performance regression in large class hierarchies.
Virtual destructors do not affect move semantics, but they do suppress implicit generation of move members in some older compilers (pre-C++14 DR). Always declare move members explicitly when a class has a virtual destructor.
// C++11 — explicit defaulted moves alongside virtual destructor
struct AbstractBase {
virtual ~AbstractBase() = default;
AbstractBase(AbstractBase&&) noexcept = default;
AbstractBase& operator=(AbstractBase&&) noexcept = default;
};Move-Only Types in STL Containers
std::vector and other sequence containers can store move-only types as long as the operations performed require only move, not copy. This includes push_back(std::move(x)), emplace_back, and erase, but excludes copy-constructing operations such as assigning one container to another by value.
// C++11
std::vector<std::unique_ptr<int>> ptrs;
ptrs.push_back(std::make_unique<int>(1)); // move into vector
ptrs.emplace_back(std::make_unique<int>(2));
// Moving the container itself is fine — no element copies
auto ptrs2 = std::move(ptrs); // O(1), ptrs is now empty
// Copying the container is a compile-time error — unique_ptr is not copyable
// auto ptrs3 = ptrs2; // error: call to deleted copy constructor
// C++17 — std::map with move-only values
std::map<int, std::unique_ptr<std::string>> registry;
registry.emplace(1, std::make_unique<std::string>("one"));
registry[2] = std::make_unique<std::string>("two");
// Extract a node and reinsert (C++17 node-handle API) — no allocation, no copy
auto node = registry.extract(1);
node.key() = 10;
registry.insert(std::move(node));std::deque, std::list, std::set, and std::unordered_map accept move-only types under the same rule. std::array requires the element type to be movable for aggregate move operations.
Sink Parameters: Pass-by-Value vs Move
When a function always stores or forwards its argument, a by-value "sink" parameter is a clean alternative to two overloads (const& and &&). The caller either copies into the parameter (lvalue) or moves into it (rvalue), and the function then moves from the local copy into storage — at most one extra move.
// C++11
// Two-overload approach — necessary when the extra move is too expensive
class Registry {
std::vector<std::string> names_;
public:
void add(const std::string& s) { names_.push_back(s); } // copy
void add(std::string&& s) { names_.push_back(std::move(s)); } // move
};
// Sink approach — single overload, simpler, one extra move for rvalue callers
class Registry2 {
std::vector<std::string> names_;
public:
void add(std::string s) { names_.push_back(std::move(s)); }
// ^^^^^^^^ by value — copy-or-move on entry, then move into storage
};
// Caller perspective
Registry2 r;
std::string name = "alice";
r.add(name); // one copy (into s) + one move (into names_)
r.add("bob"); // one move (prvalue directly into s) + one move (into names_)
r.add(std::move(name)); // one move (into s) + one move (into names_)The sink pattern costs one extra move compared to the two-overload approach. Prefer two overloads only when the type is expensive to move (rare), or when the overload set is used in a hot loop. For constructors that initialise member fields directly, the sink pattern almost always produces the best codegen after inlining.
// C++14 — sink in constructor, member initialised with a single move
struct Config {
std::string host_;
int port_;
Config(std::string host, int port) // host taken by value
: host_(std::move(host)) // one move into member
, port_(port) {}
};
Config c("localhost", 8080); // "localhost" — one move total (prvalue directly optimised)Move Semantics and Coroutines
C++20 coroutines interact with move semantics at two points: argument lifetime and promise/awaitable design. By-value parameters of a coroutine are move-constructed into the coroutine frame at the point of the initial suspension; after that the original is gone, so callers must not retain references to arguments passed by value.
// C++20
#include <coroutine>
#include <string>
// A minimal generator that demonstrates by-value capture in a coroutine frame
struct Generator {
struct promise_type {
std::string current_;
Generator get_return_object() { return Generator{std::coroutine_handle<promise_type>::from_promise(*this)}; }
std::suspend_always initial_suspend() noexcept { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
std::suspend_always yield_value(std::string s) {
current_ = std::move(s); // move into promise storage
return {};
}
void return_void() {}
void unhandled_exception() { std::terminate(); }
};
std::coroutine_handle<promise_type> handle_;
~Generator() { if (handle_) handle_.destroy(); }
bool next() { handle_.resume(); return !handle_.done(); }
const std::string& value() const { return handle_.promise().current_; }
};
Generator words(std::string prefix) {
// prefix is moved into the coroutine frame — the caller's copy is gone
co_yield prefix + "1"; // string concatenation, then move into promise
co_yield prefix + "2";
}Awaitables that store continuations must be move-constructible when placed in a std::vector or other container. Design operator co_await return types as lightweight handles (pointer + handle), not owning copies of large state.
// C++20 — awaitable move design principle
struct Task {
struct promise_type { /* ... */ };
std::coroutine_handle<promise_type> coro_;
Task(Task&& o) noexcept : coro_(std::exchange(o.coro_, {})) {} // move-only
Task(const Task&) = delete;
~Task() { if (coro_) coro_.destroy(); }
};Never co_await a moved-from awaitable; the handle will be null and resume() is undefined behaviour.
See Also
- Move Semantics — Basics — rvalue references, move constructors, basic patterns
- Value Categories — lvalue, prvalue, xvalue, glvalue, rvalue in depth
- noexcept — specifier, operator, conditional noexcept
- Perfect Forwarding — reference collapsing patterns and forwarding pitfalls