conceptModern C++~4 min readUpdated 2026-07-02#cpp#move-semantics#value-categories#rvalue-references#ownership

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::move from a const object usually does not move. A move constructor normally needs T&&, not const 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 noexcept can 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, and std::unique_ptr own 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::move at 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 take T&& 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::move as 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/