Polyglot
Languages C++ move semantics
C++ § move-semantics

Move semantics

Move semantics, introduced in C++11, distinguishes the case where a value is copied (a new independent instance is constructed) from the case where it is moved (the resources of an existing instance are transferred to the new one, leaving the source in a valid but unspecified state). The distinction enables efficient ownership transfer for resource-owning types: a std::vector returned from a function need not have its elements copied; a std::unique_ptr passed to a function need not allocate a second managed object. The mechanism is the foundation of the modern C++ idioms around returning containers by value, sink parameters, and writing generic code with no unnecessary copies.

The mechanism rests on three pieces: a value category system that distinguishes lvalues, rvalues, and several refinements; rvalue references (T&&) that bind to rvalues; and the corresponding constructors and assignment operators that the standard library and user code define to take advantage. The mechanics interact with overload resolution, with template type deduction, and with the noexcept specification, in ways that are not always obvious. This page covers the principal cases.

Lvalues, rvalues, and the value-category model

Before C++11 the distinction was binary: lvalues (objects with names; addressable) and rvalues (everything else). C++11 refined the model into five categories, organised in a tree:

        expression
       /          \
   glvalue       rvalue
   /     \      /     \
lvalue  xvalue       prvalue
  • lvalue — an expression that designates an object: a name, *p, arr[i], s.m. Has an identity, is not movable from automatically.
  • prvalue (pure rvalue) — an expression that yields a value: a literal 42, x + 1, the result of a non-reference-returning function. Has no identity.
  • xvalue (eXpiring value) — an expression that designates an object whose resources may be reused: std::move(x), the result of a function returning T&&. Has an identity, is movable from.
  • glvalue (generalised lvalue) — lvalue or xvalue. Has an identity.
  • rvalue — prvalue or xvalue. Movable from.

The practical distinctions:

CategoryExamplesMay be moved from?
lvaluex, *p, arr[i]No (without explicit cast)
xvaluestd::move(x), static_cast<T&&>(x)Yes
prvalue42, x + 1, Widget()Yes (in many contexts; copy elision often makes the question moot)

The terminology is awkward but the distinction matters: the move-construction overload is selected only for rvalue arguments (xvalue or prvalue), preserving the lvalue case for ordinary copy.

Rvalue references

An rvalue reference, written T&&, is a reference type that binds to rvalues:

int  a = 0;
int  &lref  = a;             // OK: lvalue reference to an lvalue
int  &&rref = 42;            // OK: rvalue reference to an rvalue (a prvalue)
int  &&bad  = a;             // ERROR: cannot bind rvalue reference to lvalue
int  &&from = std::move(a);  // OK: explicit cast to rvalue

The principal use is in declarations of move constructors and move assignment operators:

class Widget {
public:
    Widget(Widget &&other) noexcept;             // move constructor
    Widget &operator=(Widget &&other) noexcept;  // move assignment
};

When such a constructor is invoked with an rvalue argument, overload resolution selects the Widget&& overload over the const Widget& (copy constructor) overload.

Move constructors and move assignment

A move constructor takes a T&& parameter and transfers the resources from the source:

class StringHolder {
public:
    StringHolder(std::string s) : data_(std::move(s)) {}

    StringHolder(StringHolder &&other) noexcept
        : data_(std::move(other.data_))
    {}

    StringHolder &operator=(StringHolder &&other) noexcept {
        if (this != &other) {
            data_ = std::move(other.data_);
        }
        return *this;
    }

private:
    std::string data_;
};

The transfer is implementation-defined; for std::string, it is typically a swap of internal pointers, leaving the source with an empty (or otherwise unspecified-but-valid) buffer. The contract:

  • The source remains in a valid but unspecified state. It can be assigned to, destroyed, and queried; the contents are not promised to be the contents the source held before the move.
  • The move constructor and move assignment should be noexcept whenever possible. The standard library’s containers (notably std::vector) detect the noexcept and use moves rather than copies during reallocation; without noexcept, they fall back to copies for strong-exception-safety reasons.

std::move and the conversion to rvalue

std::move is a cast: it converts its argument to an xvalue, signalling to overload resolution that the argument may be moved from. It does not actually move anything; the move is performed by the constructor or assignment operator that receives the xvalue.

std::string a = "hello";
std::string b = std::move(a);    // a is now in a valid-but-unspecified state
                                  // b holds "hello"

The implementation of std::move is a single line:

template <typename T>
constexpr std::remove_reference_t<T>&& move(T &&t) noexcept {
    return static_cast<std::remove_reference_t<T>&&>(t);
}

The cast is the entire mechanism. std::move is not a function in any deeper sense; it is a notational helper for the cast.

The moved-from state

After a move, the source is in a valid but unspecified state. The standard library’s types document this: a moved-from std::vector is empty; a moved-from std::unique_ptr is null; a moved-from std::string may or may not be empty.

The discipline:

  • A moved-from object can be assigned to, destroyed, queried about its size or state.
  • A moved-from object cannot be assumed to hold any particular value.
  • A moved-from object can be re-assigned and made useful again.
std::vector<int> v = {1, 2, 3, 4, 5};
auto             v2 = std::move(v);

// v is in a valid-but-unspecified state.
// Typical implementations leave it empty:
v.size();              // probably 0; not guaranteed
v.push_back(42);       // OK; v is usable again

User-defined types should preserve the valid part: a moved-from instance must be safely destructible and assignable. The simplest way to guarantee this is to use the rule-of-zero: let the standard-library member types provide the move semantics, and write no special functions.

Forwarding references

When && appears in a deduced template parameter (T&& where T is the template parameter), it is not an rvalue reference; it is a forwarding reference. The deduced type depends on the value category of the argument:

template <typename T>
void wrapper(T &&arg) {
    // If called with an lvalue X, T is X& and arg is X&.
    // If called with an rvalue X, T is X  and arg is X&&.
}

int x = 0;
wrapper(x);             // T = int&,  arg is int&
wrapper(42);            // T = int,   arg is int&&
wrapper(std::move(x));  // T = int,   arg is int&&

The forwarding reference admits perfect forwarding: arg can be passed onward to another function, preserving its original value category. The conventional tool for the forwarding is std::forward:

template <typename T, typename Arg>
std::unique_ptr<T> make_widget(Arg &&arg) {
    return std::make_unique<T>(std::forward<Arg>(arg));
}

std::forward<Arg>(arg) casts to Arg&&, which collapses to either Arg& or Arg&& depending on whether Arg is itself a reference type. The construction lets generic factories pass arguments through to constructors with no copies introduced.

Reference collapsing

The reference-collapsing rules are what make forwarding references work:

TypeCollapses to
T& &T&
T& &&T&
T&& &T&
T&& &&T&&

In template type deduction, when T = U& (the template parameter is deduced as a reference type), T&& becomes U& after collapsing. The rule is what permits a single T&& parameter to bind to both lvalues (where T is deduced as U&) and rvalues (where T is deduced as U).

The mechanism is invisible in the source — the programmer writes T&& and std::forward<T>(arg) — but the rule is what makes the perfect-forwarding pattern work.

Copy elision and return-value optimisation

The standard permits, and in some cases requires, the compiler to elide copies and moves entirely:

  • Return-value optimisation (RVO): a function that returns a temporary by value need not actually construct the temporary; the return-slot is constructed in place.
  • Named return-value optimisation (NRVO): a function that returns a named local variable may be optimised to construct that local in the return-slot.
  • Copy elision in mandatory cases (C++17): the construction of a prvalue into another prvalue is required to be elided.
std::vector<int> make_data() {
    std::vector<int> v;
    v.push_back(1);
    return v;            // RVO; no copy or move
}

std::vector<int> data = make_data();   // mandatory copy elision (C++17)
                                        // for the return-into-data case

The consequence: returning by value in modern C++ is essentially free. The conventional advice is to return by value whenever a function produces a value; the compiler handles the optimisation. Pre-C++11 code that used out-parameters to avoid copies is mostly obsolete.

Why moves matter

The performance story:

  • A function that returns std::vector<int> constructs the vector in the caller’s slot directly; no copy occurs (RVO/elision).
  • A function that takes std::string by value may take it via move from a temporary at the call site, with no allocation.
  • std::vector reallocation moves elements rather than copying them, when the element type’s move constructor is noexcept.

A canonical example is the sink parameter:

class Document {
public:
    Document(std::string title)              // by value: caller chooses to copy or move
        : title_(std::move(title))           // move into the member
    {}

private:
    std::string title_;
};

std::string s = compute_title();
Document d(std::move(s));            // moves s into the parameter, then into the member
                                       //   — zero copies of the string data

The pattern — take by value, move into the member — is the canonical sink-parameter idiom. It admits move-from-rvalue (no copies) and copy-from-lvalue (one copy) at the caller’s discretion.

The interaction with noexcept

The standard library’s std::vector reallocates by moving its elements only if the element’s move constructor is noexcept. If the move constructor is not noexcept, the vector falls back to copying — because if a move could throw partway through, the strong exception guarantee would be violated.

The consequence is that user-defined types should mark their move constructors noexcept whenever possible:

class Widget {
public:
    Widget(Widget &&other) noexcept;            // can be moved during vector growth
    Widget &operator=(Widget &&other) noexcept;
};

The noexcept is a substantive performance improvement; it is also a discipline forcing the move constructor to be implemented in a way that genuinely cannot throw (typically: pointer swaps, integer assignments, no allocation).

A note on when not to move

std::move should be applied to a value only when the program no longer needs the source’s contents. The principal mistakes:

  • return std::move(local) — defeats RVO/NRVO. The compiler is required to perform the move (or copy) explicitly, which is the same cost as RVO at best and worse at worst. Bare return local is correct.
  • std::move of a const object — has no effect, because the rvalue reference cannot bind to a const-qualified object, and the cast falls back to const-lvalue-reference.
  • std::move immediately followed by reading the source — the source is in an unspecified state.

The discipline is to apply std::move at the boundary where ownership transfers and not elsewhere. The convention in modern C++ is that explicit std::move calls are uncommon; the language and the standard library handle most cases through value-by-default and RVO.