Move Semantics
Transfer resources from expiring objects instead of copying. Foundation of O(1) container operations, unique ownership, and efficient return values.
Move Semanticssince C++11A language mechanism that transfers an object's resources to another object instead of copying them, triggered when the source is an rvalue (temporary or explicitly expiring) expression.
Overview
Before C++11, returning a large object from a function, inserting into a container, or transferring ownership of a heap resource all implied deep copies. Move semantics eliminate that overhead by letting the compiler β or programmer β signal "this object is about to expire; take its internals directly."
Value categories are the formal basis. Every expression belongs to exactly one:
| Category | Has identity | Movable | Typical examples |
|---|---|---|---|
| lvalue | yes | no | named variable, *p, a[i], ++i |
| xvalue | yes | yes | std::move(x), function returning T&& |
| prvalue | no | yes | 42, T{}, function returning T by value |
| glvalue | yes | β | lvalue βͺ xvalue |
| rvalue | β | yes | xvalue βͺ prvalue |
An rvalue reference (T&&, C++11) binds to xvalues and prvalues but not lvalues. This asymmetry is load-bearing: it forces callers to be explicit about relinquishing ownership via std::move, preventing accidental resource theft.
std::move (C++11) is a cast, not an action. It unconditionally produces an xvalue, enabling move constructor or move assignment to be chosen by overload resolution. The actual resource transfer happens inside those special member functions.
Copy elision and implicit moves interact with move semantics on return paths:
- Mandatory copy elision (C++17): a prvalue initializer is never materialized into a temporary β the object is constructed directly in its final destination.
Widget w = Widget(args)is guaranteed zero copies and zero moves regardless ofnoexceptor move constructor existence. - NRVO (Named Return Value Optimization): still a quality-of-implementation optimization (not mandated), but reliably applied by all major compilers at
-O1+. When NRVO fires, no copy or move happens. - Implicit move on
return: C++11 treats a named local variable in areturnexpression as an rvalue. C++20 (P1825) extended this to rvalue reference parameters andthrowoperands. C++23 (P2266) broadened it further to cases previously rejected by the two-step overload resolution.
Syntax
T&& // rvalue reference β binds rvalues only (C++11)
template<typename T>
void f(T&& x); // forwarding reference β T deduced; binds anything (C++11)
std::move(expr) // cast to T&& β signals "expendable" (C++11)
std::forward<T>(x) // preserve original value category in templates (C++11)
std::exchange(src, new_val) // returns old value, assigns new β useful in move ops (C++14)A forwarding reference (T&& with deduced T) is not an rvalue reference. Inside f, x is always an lvalue β it has a name. Use std::forward<T>(x) to restore the caller's value category.
Examples
Implementing move operations with std::exchange
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_; }
// C++11: move constructor
Buffer(Buffer&& other) noexcept
: data_(std::exchange(other.data_, nullptr)) // C++14: std::exchange
, size_(std::exchange(other.size_, 0))
{}
// C++11: move assignment
Buffer& operator=(Buffer&& other) noexcept {
if (this == &other) return *this; // self-move guard
delete[] data_;
data_ = std::exchange(other.data_, nullptr);
size_ = std::exchange(other.size_, 0);
return *this;
}
Buffer(const Buffer&) = delete;
Buffer& operator=(const Buffer&) = delete;
};std::exchange (C++14) reads the old value and assigns the new value atomically in one expression, removing the read-then-nullify pattern that can leave the source in a half-valid state if interrupted mentally.
Rule of Zero β prefer this when possible
Wrapping raw resources in standard library types lets the compiler generate all five operations correctly and with appropriate noexcept automatically:
class Pipeline {
std::vector<std::unique_ptr<Stage>> stages_; // movable, non-copyable
std::string name_; // movable, copyable
// No destructor, no special member functions declared.
// Compiler generates correct move ctor, move assign (both noexcept),
// and deletes copy ctor/copy assign (because unique_ptr is non-copyable).
};When you must declare a destructor but want compiler-generated moves, = default them explicitly:
class Handle {
int fd_ = -1;
public:
~Handle() { if (fd_ >= 0) ::close(fd_); }
Handle(Handle&&) noexcept = default; // C++11
Handle& operator=(Handle&&) noexcept = default; // C++11
Handle(const Handle&) = delete;
Handle& operator=(const Handle&) = delete;
};Perfect forwarding in factory functions
// C++11: Args&&... are forwarding references
template<typename T, typename... Args>
T* arena_construct(Arena& arena, Args&&... args) {
void* mem = arena.alloc(sizeof(T), alignof(T));
return ::new(mem) T(std::forward<Args>(args)...);
}
std::string label = "sensor";
// label is an lvalue β forwarded as lvalue, Widget gets a copy
Widget* w1 = arena_construct<Widget>(arena, label, 1);
// std::move(label) is an xvalue β forwarded as rvalue, Widget moves from it
Widget* w2 = arena_construct<Widget>(arena, std::move(label), 2);
// "temp" is a prvalue β forwarded as rvalue, Widget constructs in-place
Widget* w3 = arena_construct<Widget>(arena, "temp", 3);Return paths: let the compiler work
// C++17: mandatory copy elision β prvalue, zero copies/moves guaranteed
Widget make_a() {
return Widget{"sensor", 42};
}
// NRVO: compiler constructs w directly in caller's storage (not mandated, but universal)
Widget make_b() {
Widget w{"sensor", 42};
// ...populate...
return w;
}
// WRONG: explicit move inhibits NRVO, forces a move where elision was possible
Widget make_bad() {
Widget w{"sensor", 42};
return std::move(w); // never do this
}Best Practices
- Always mark move operations
noexceptwhen they genuinely cannot throw.std::vectoruses the move constructor during reallocation only if it isnoexcept; otherwise it copies to preserve the strong exception guarantee β defeating the entire purpose. - Prefer Rule of Zero: if
unique_ptr,vector, or a custom RAII wrapper manages your resource, the compiler generates correct,noexceptmoves automatically. Write move operations by hand only when managing a raw resource directly. - Never
std::movea local inreturn: NRVO and mandatory copy elision are strictly better. C++20 implicit move handles the remaining cases. Explicitstd::moveon a local return is always a pessimization. - Declare all five or none: declaring any of {destructor, copy constructor, copy assignment} suppresses implicit generation of move operations (C++11 rule). Declare the missing ones explicitly, even if as
= defaultor= delete.
Common Pitfalls
Using a moved-from object
The standard guarantees a moved-from object is in a "valid but unspecified" state. Only operations with no preconditions are safe on it: assignment and destruction. Anything else β including reading a member β has unspecified behavior.
std::vector<int> src(1'000'000, 0);
std::vector<int> dst = std::move(src);
src.size(); // valid: returns 0 on all implementations, but not guaranteed
src.push_back(1); // safe: push_back has no precondition on existing content
// src[0] // undefined behavior if src is empty
src = {}; // safe: assignment has no preconditions β restores to known stateForwarding references are not rvalue references
template<typename T>
void sink(T&& x) {
process(std::move(x)); // WRONG if caller passed an lvalue β silently moves it
}
template<typename T>
void forward_correctly(T&& x) {
process(std::forward<T>(x)); // correct: preserves caller's value category
}Inside sink, x is an lvalue (it has a name). std::move(x) unconditionally casts it to rvalue and moves from the caller's object even when the caller did not intend to relinquish it. std::forward is the correct tool inside forwarding templates.
const T&& β almost never intentional
const T&& binds to rvalues but cannot be moved from β move constructors require a non-const T&&. The compiler silently falls back to the copy constructor, making the && annotation misleading and inert.
Missing noexcept on move β container pessimisation
// Without noexcept, std::vector<Widget> copies during reallocation
struct Widget {
Widget(Widget&&); // no noexcept β vector treats it as potentially throwing
Widget(const Widget&); // copy used instead during push_back growth
};
struct WidgetFast {
WidgetFast(WidgetFast&&) noexcept; // vector moves during reallocation
};The performance difference is O(n) copies per reallocation event for the first type versus O(1) pointer swaps for the second.
See Also
- RAII β move semantics compose with RAII: movable handles give transferable, unique ownership
- std::unique_ptr β the canonical move-only RAII type; models exclusive ownership
- Perfect Forwarding β
std::forwardand forwarding references in depth - Type Erasure β move-only type-erased wrappers (e.g.,
std::move_only_function, C++23)