Move semantics and value categories
Move semantics let a C++ object transfer resources instead of duplicating them. A
std::vector move can steal three pointers; a copy has to allocate and copy elements. The
language decides whether moving is even available through value categories: lvalues have
identity, prvalues are pure computed values, and xvalues have identity but are eligible to
be treated as expiring. The daily rule is simple: std::move(x) does not move anything by
itself; it says "this named object may be treated as expiring, so select move operations if
they exist."
The reset: move semantics are ownership transfer plus overload resolution. They are not a magical fast copy.
How it really works
C++11 added rvalue references, written T&&, and move constructors/assignments. A type
that owns a resource can define:
Buffer(Buffer &&other) noexcept;
Buffer &operator=(Buffer &&other) noexcept;
Those operations usually copy small metadata, steal a pointer or handle, and leave the source object valid but empty. A moved-from object must still be destructible and assignable unless the type documents a narrower contract. The standard library describes moved-from objects as valid but unspecified, which means you can destroy them, assign to them, or call operations without preconditions; you cannot assume their old value remains.
Value categories decide which overload is viable:
| Expression | Category idea | Example | Consequence |
|---|---|---|---|
| named variable | lvalue | buffer |
binds to T& / const T& |
| temporary value | prvalue | Buffer{4} |
can initialize directly or bind to T&& |
| expiring object | xvalue | std::move(buffer) |
can bind to T&& and move |
| named rvalue-reference parameter | lvalue | other inside Buffer(Buffer&& other) |
needs std::move(other.member) to move members |
This last row is the classic gotcha. A parameter declared Buffer&& other has a name, so
the expression other is an lvalue. Inside a move constructor, member moves must be
explicit:
Buffer(Buffer &&other) noexcept
: size_(std::exchange(other.size_, 0)),
data_(std::move(other.data_)) {}
noexcept matters because containers such as std::vector need to preserve strong
exception guarantees during reallocation. If moving an element might throw and copying is
available, the container may copy instead of move. For resource-owning types, moves should
usually be noexcept.
Executable artifact: count copies and moves
The runnable demo lives in
examples/modern-cpp/move-semantics-and-value-categories/. It defines a heap-owning
Buffer with explicit copy and move operations, then pushes lvalues, moved lvalues, and
temporaries into a vector.
cd examples/modern-cpp/move-semantics-and-value-categories
./run.sh
The key operations are:
buffers.push_back(first); // first is an lvalue, so copy
buffers.push_back(std::move(first)); // xvalue, so move
buffers.push_back(Buffer{2}); // temporary, so move or direct construction
The move constructor steals the pointer and empties the source:
Buffer(Buffer &&other) noexcept
: size_(std::exchange(other.size_, 0)),
data_(std::move(other.data_)) {
++moves;
}
The script also writes optimized assembly:
g++ -std=c++20 -O2 -S demo.cpp -o demo.O2.s
For a trivial pointer-owning type, a move typically lowers to pointer copies, nulling the source metadata, and destructor calls on the final owners. A copy lowers to allocation and element copying. That is the whole performance story: move is fast only when the type's move operation can transfer a representation cheaply.
Failure modes & trade-offs
std::movefrom aconstobject usually does not move. A move constructor normally needsT&&, notconst T&&, because it must modify the source.- Moved-from is valid, not meaningfully valued. Checking
empty()is fine if the type documents it. Assuming the old contents remain is not. - Forgetting
noexceptcan turn moves into copies. Standard containers may prefer copy during reallocation if move can throw and copy is available. - Self-move needs a sane result. Standard-library types tolerate self-move assignment as valid but unspecified. Your own move assignment should avoid double-free and broken invariants.
- Copy elision can hide moves. Returning a local by value may construct directly in the
caller. That is good; do not add
std::move(local)to a return statement unless you know why, because it can block elision. - Moving a pointer does not move the pointee. If a type stores external references, moving the object may only transfer handles. Aliasing and lifetime still need design.
In practice
- Prefer rule of zero. If standard members such as
std::vector,std::string, andstd::unique_ptrown the resources, let their special members do the work. - Write all special members when you own raw resources. Destructor, copy, move, and assignments are a set. A raw owning pointer with only a destructor is a future double-free.
- Use
std::moveat ownership boundaries. Moving into a member, container, or return object is useful. Sprinkling it everywhere makes code harder to read and can pessimize. - Treat rvalue references as API meaning.
T&&usually means "this function may consume the argument." Do not takeT&&just to avoid a copy if the function only reads. - Inspect the generated code when performance is the claim. Move semantics are a source
transformation that should become fewer allocations and copies. Check with Compiler
Explorer,
-S, or a profiler.
Connects to: Smart pointers: unique_ptr, shared_ptr, ownership · RAII: the idea that changes everything · Structs, unions, and bitfields · The heap: malloc/free and the allocator underneath · Memory leaks & ownership discipline · Optimization: what -O2 does to your code
Sources
- cppreference - Value categories - lvalue, prvalue, xvalue, glvalue, and how expressions bind. https://en.cppreference.com/w/cpp/language/value_category
- cppreference -
std::move-std::moveas a cast to an xvalue and the valid-but-unspecified moved-from rule. https://en.cppreference.com/w/cpp/utility/move - cppreference - Move constructors - move constructor declaration, generation, deletion, and examples. https://en.cppreference.com/w/cpp/language/move_constructor
- cppreference - The rule of three/five/zero - why resource-owning types need coherent special member behavior. https://en.cppreference.com/w/cpp/language/rule_of_three
- C++ working draft -
[class.copy.ctor]- standard wording for copy and move constructors. https://eel.is/c++draft/class.copy.ctor - C++ working draft -
[lib.types.movedfrom]- standard-library moved-from object requirements. https://eel.is/c++draft/lib.types.movedfrom - Compiler Explorer - inspect whether a move actually removes allocation or copying for a concrete type and optimizer. https://godbolt.org/