Skip to content
C++
Language
since C++11
Intermediate

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++11

A 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:

CategoryHas identityMovableTypical examples
lvalueyesnonamed variable, *p, a[i], ++i
xvalueyesyesstd::move(x), function returning T&&
prvaluenoyes42, T{}, function returning T by value
glvalueyesβ€”lvalue βˆͺ xvalue
rvalueβ€”yesxvalue βˆͺ 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 of noexcept or 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 a return expression as an rvalue. C++20 (P1825) extended this to rvalue reference parameters and throw operands. C++23 (P2266) broadened it further to cases previously rejected by the two-step overload resolution.

Syntax

cpp
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

cpp
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:

cpp
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:

cpp
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

cpp
// 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

cpp
// 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 noexcept when they genuinely cannot throw. std::vector uses the move constructor during reallocation only if it is noexcept; 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, noexcept moves automatically. Write move operations by hand only when managing a raw resource directly.
  • Never std::move a local in return: NRVO and mandatory copy elision are strictly better. C++20 implicit move handles the remaining cases. Explicit std::move on 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 = default or = 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.

cpp
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 state

Forwarding references are not rvalue references

cpp
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

cpp
// 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::forward and forwarding references in depth
  • Type Erasure β€” move-only type-erased wrappers (e.g., std::move_only_function, C++23)