Move Semantics and Value Categories

This section documents C++23 (ISO/IEC 14882:2024), as published by ISO/IEC JTC1/SC22/WG21 (wg21), verified against the freely available working draft N5046 (eel.is/c++draft) and cppreference.com.

This content was generated with the assistance of AI and should be verified against the working draft and cppreference.com before being relied on in production.

This section’s bibliography lists the reference material consulted while preparing these pages.

Value categories (lvalue/prvalue/xvalue) were introduced in Operators and Expressions; this page covers what they make possible: transferring ownership of a resource instead of copying it.

Rvalue References and std::move

An rvalue reference (T&&) binds only to rvalues (temporaries, or anything explicitly cast to one), which is exactly the signal a type needs to safely "steal" from its argument instead of copying:

#include <utility>
#include <vector>

class Buffer {
public:
    explicit Buffer(std::size_t size) : size_(size), data_(new int[size]) {}
    ~Buffer() { delete[] data_; }

    Buffer(const Buffer& other)                       // copy: allocates a new buffer, copies elements
        : size_(other.size_), data_(new int[other.size_]) {
        std::copy(other.data_, other.data_ + size_, data_);
    }

    Buffer(Buffer&& other) noexcept                     // move: steals the pointer, leaves other empty
        : size_(other.size_), data_(other.data_) {
        other.size_ = 0;
        other.data_ = nullptr;
    }

private:
    std::size_t size_;
    int* data_;
};

int main() {
    Buffer a(1000);
    Buffer b = std::move(a);   // a's buffer is now owned by b; a is left in a valid-but-unspecified,
                                // "moved-from" state -- safe to destroy or reassign, but not to read
}

std::move does not itself move anything — it is purely a static_cast<T&&>, i.e. it casts its argument to an xvalue so overload resolution picks the move overload instead of the copy overload.

std::forward and Perfect Forwarding

In a template, a forwarding reference (T&& where T is deduced) can bind to either an lvalue or an rvalue; std::forward<T> preserves which one it originally was when passing it along, so a generic wrapper doesn’t force an unwanted copy:

#include <utility>
#include <memory>

template <typename T, typename... Args>
std::unique_ptr<T> makeUniqueLike(Args&&... args) {           // Args&& here are forwarding references
    return std::make_unique<T>(std::forward<Args>(args)...);   // forwards each argument as it originally was
}

struct Point { Point(int, int) {} };
// makeUniqueLike<Point>(1, 2) forwards two rvalues (int literals) without any unnecessary copy

Do not confuse T&& in a template with deduced T (a forwarding reference) with T&& where T is a concrete, non-deduced type (an ordinary rvalue reference, as in Buffer(Buffer&& other) above) — the syntax looks identical but the binding rules differ.

Copy Elision and Guaranteed Elision

Since C++17, returning a temporary by value from a function is guaranteed to construct directly in the caller’s storage — no copy or move constructor is even required to exist, let alone called:

Buffer makeBuffer() {
    return Buffer(1000);        // guaranteed elision (C++17): constructed directly into the caller's variable
}

int main() {
    Buffer buf = makeBuffer();   // no copy, no move -- Buffer is built in buf's storage directly
}

Named Return Value Optimization (NRVO) — eliding the copy/move of a named local returned from a function — is still only an optional (though near-universally applied) optimization, not guaranteed by the standard:

Buffer makeBufferNamed() {
    Buffer local(1000);
    return local;              // NRVO likely applies (not guaranteed) -- and even if it didn't, "local" is an
}                                 // lvalue that the compiler still implicitly treats as movable-from here

noexcept Moves

std::vector (and other containers) only move elements during reallocation if the move constructor is noexcept — otherwise it falls back to copying, to preserve the strong exception-safety guarantee (a reallocation that fails partway through must not leave the original data corrupted):

class SafeToMove {
public:
    SafeToMove(SafeToMove&&) noexcept = default;      // vector will move these on reallocation
    SafeToMove(const SafeToMove&) = default;
};

class RiskyToMove {
public:
    RiskyToMove(RiskyToMove&&) = default;               // NOT noexcept -- vector will copy these instead,
    RiskyToMove(const RiskyToMove&) = default;            // even though a move constructor exists
};

Always mark a move constructor/assignment noexcept when it genuinely cannot throw (which is nearly always, since a move typically just swaps a few pointers/scalars).

Returning by Value

Given guaranteed elision, returning a large object by value is now the idiomatic, efficient choice in most cases — there is rarely a reason to reach for an output parameter (void compute(Result& out)) purely for performance:

#include <vector>

std::vector<int> computeSquares(int n) {
    std::vector<int> result;
    result.reserve(n);
    for (int i = 0; i < n; ++i) result.push_back(i * i);
    return result;              // elided or, worst case, moved -- never deep-copied
}

Copy vs. Move of a Buffer-Owning Object

Copying a buffer-owning object allocates a second buffer and duplicates the data; moving it transfers the pointer and leaves the source empty

See Also