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
}

Rule of Five

Rule of Fivesince C++11

If a class explicitly defines any one of: destructor, copy constructor, copy assignment operator, move constructor, or move assignment operator β€” it must explicitly define all five. Neglecting any member leaves the class with silently incorrect copy or move behavior.

The core problem is that C++11's generation rules are asymmetric. Declaring a destructor suppresses implicit generation of move operations. Declaring a move operation deletes the copy operations. Trusting the compiler defaults for "the other three" in a resource-owning class is a latent bug.

Compiler generation table (C++11):

User declaresMove ctorMove assignCopy ctorCopy assign
Nothinggeneratedgeneratedgeneratedgenerated
Destructor onlysuppressedsuppressedgenerated (deprecated)generated (deprecated)
Copy constructorsuppressedsuppressedβ€”generated (deprecated)
Copy assignmentsuppressedsuppressedgenerated (deprecated)β€”
Move constructorsuppressedsuppresseddeleteddeleted
Move assignmentsuppressedsuppresseddeleteddeleted

A class managing a raw heap buffer with all five members written explicitly:

cpp
#include <cstring>
#include <cstddef>
#include <utility>    // std::exchange (C++14)

class Buffer {
    std::byte*  data_;
    std::size_t size_;

public:
    explicit Buffer(std::size_t n)
        : data_(new std::byte[n]), size_(n) {}

    // 1. Destructor
    ~Buffer() { delete[] data_; }

    // 2. Copy constructor β€” deep copy
    Buffer(const Buffer& other)
        : data_(new std::byte[other.size_])
        , size_(other.size_)
    {
        std::memcpy(data_, other.data_, size_);
    }

    // 3. Copy assignment β€” strong exception safety via copy-and-swap
    Buffer& operator=(const Buffer& other) {
        Buffer tmp(other);          // copy to temporary; throws here, not below
        std::swap(data_, tmp.data_);
        std::swap(size_, tmp.size_);
        return *this;
    }

    // 4. Move constructor β€” noexcept: critical for std::vector reallocation
    Buffer(Buffer&& other) noexcept                     // C++11
        : data_(std::exchange(other.data_, nullptr))    // C++14
        , size_(std::exchange(other.size_, 0))
    {}

    // 5. Move assignment β€” noexcept: allows std::vector to move instead of copy
    Buffer& operator=(Buffer&& other) noexcept {        // C++11
        if (this == &other) return *this;               // self-move guard
        delete[] data_;
        data_ = std::exchange(other.data_, nullptr);    // C++14
        size_ = std::exchange(other.size_, 0);
        return *this;
    }
};

Best Practice. Declare all five explicitly even when some are = default or = delete. It documents intent and prevents surprises when the class is modified later.

The noexcept on the move constructor and move assignment is not optional decoration β€” it is the mechanism by which std::vector selects move over copy during reallocation. See the next section.


noexcept and std::vector Reallocation

When std::vector grows its internal buffer, it must transfer every element from the old allocation to the new one. It has two choices:

  • Move each element: O(1) per element, O(n) total, but if any move throws midway, the old buffer is already corrupted. std::vector cannot provide the strong exception guarantee.
  • Copy each element: O(n) per element, O(nΒ²) total in the worst case, but copying can be undone β€” so the strong guarantee is preserved.

std::vector resolves this with std::move_if_noexcept, which moves only when the move constructor is noexcept. If not, it copies unconditionally.

cpp
#include <type_traits>  // std::is_nothrow_move_constructible_v (C++17)

struct Slow {
    Slow(Slow&&);                    // not noexcept β€” vector COPIES during reallocation
    Slow(const Slow&);
};

struct Fast {
    Fast(Fast&&) noexcept;           // noexcept β€” vector MOVES during reallocation
    Fast(const Fast&);
};

// C++17: check at compile time
static_assert(!std::is_nothrow_move_constructible_v<Slow>);  // copies on realloc
static_assert( std::is_nothrow_move_constructible_v<Fast>);  // moves on realloc

The performance difference is stark. A std::vector<Slow> that doubles its capacity makes O(n) deep copies of every existing element. A std::vector<Fast> does O(n) pointer swaps β€” three machine words per element.

Common Pitfall. Allocating memory in the move constructor makes it potentially throwing and removes noexcept. The invariant to maintain: move operations should only swap or transfer pointers, never allocate.

cpp
struct GoodMove {
    std::byte* ptr_;
    std::size_t size_;

    // Correct: only transfers ownership of existing memory
    GoodMove(GoodMove&& other) noexcept
        : ptr_(std::exchange(other.ptr_, nullptr))
        , size_(std::exchange(other.size_, 0))
    {}
};

struct BadMove {
    std::vector<int> data_;

    // WRONG: allocates a new vector β€” potentially throws β€” cannot be noexcept
    BadMove(BadMove&& other)
        : data_(other.data_)    // copying, not moving!
    {}

    // Correct:
    BadMove(BadMove&& other) noexcept
        : data_(std::move(other.data_))   // std::vector's move ctor is noexcept
    {}
};

You can add a static_assert to a class to make violation a compile error:

cpp
struct MyType {
    MyType(MyType&&) noexcept;
    // ...
};
static_assert(std::is_nothrow_move_constructible_v<MyType>,  // C++17
    "MyType move constructor must be noexcept for std::vector performance");

Perfect Forwarding (std::forward)

A wrapper function template that accepts any argument and passes it to another function must preserve the value category of that argument. Without that, every argument arrives at the target as an lvalue β€” silently copying where a move was intended.

The problem without std::forward

cpp
void sink(std::string s);   // expects to potentially move-construct s

// WRONG: always copies β€” x is an lvalue inside the function regardless of
// how the caller passed the argument
template<typename T>
void bad_wrapper(T&& x) {
    sink(x);                // x is an lvalue here β€” always copies into sink
}

std::string s = "hello";
bad_wrapper(s);              // caller passes lvalue β€” OK, copy expected
bad_wrapper(std::move(s));   // caller passes rvalue β€” but still copies! Bug.

The correct pattern with std::forward

T&& in a deduced template context is a forwarding reference, not an rvalue reference. When the caller passes an lvalue, T deduces to Arg& and T&& collapses to Arg&. When the caller passes an rvalue, T deduces to Arg and T&& stays Arg&&. std::forward<T> then restores that original category:

cpp
// C++11: T&& is a forwarding reference because T is deduced
template<typename T>
void wrapper(T&& val) {
    target(std::forward<T>(val));   // lvalue stays lvalue, rvalue stays rvalue
}

std::string s = "hello";
wrapper(s);              // T = std::string& β€” forwards as lvalue  β€” target gets ref
wrapper(std::move(s));   // T = std::string  β€” forwards as rvalue  β€” target moves from s
wrapper("world");        // T = const char*  β€” forwards as rvalue  β€” target moves/constructs

The variadic version powers emplace_back and std::make_unique:

cpp
// C++11: variadic forwarding reference β€” the engine behind emplace APIs
template<typename T, typename... Args>
std::unique_ptr<T> make_unique(Args&&... args) {
    return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}

// emplace_back forwards arguments directly into the container's storage β€”
// no temporary object is created
std::vector<std::string> v;
v.emplace_back("hello");             // constructs in-place β€” no temporary
v.emplace_back(5, 'x');              // std::string(5, 'x') β€” perfect forwarding

Best Practice. Inside a forwarding template, always use std::forward<T>(x), never std::move(x). std::move unconditionally casts to rvalue and will move from lvalue arguments, silently leaving the caller's variable in an indeterminate state.

Common Pitfall. std::forward is only meaningful inside a deduced template context. Using it outside is either redundant (std::forward<T>(rvalue) where T is known) or dangerous. It is not a general-purpose "maybe move" tool.


Moved-From State

After a move, the source object is in a "valid but unspecified" state. The standard imposes two guarantees and no others:

  1. The destructor is safe to call. The moved-from object will be destroyed without undefined behavior.
  2. Assignment is safe. You can assign a new value to restore it to a known state.

Beyond those two, any other operation has implementation-defined or unspecified results. You can call size() on a moved-from std::vector β€” it returns some valid value (typically 0 on all implementations) β€” but the standard does not promise 0 specifically.

cpp
std::string a = "hello, world";
std::string b = std::move(a);

// a is now moved-from
// These are all safe β€” they have no preconditions on existing content:
a.clear();                    // safe: clear() is always valid
a = "new value";              // safe: assignment restores known state
a.size();                     // defined but unspecified β€” may be 0, may not be

// This is NOT safe β€” undefined behavior if a is empty after the move:
// char c = a[0];             // operator[] has a precondition: index < size()

// Safe defensive pattern after using a moved-from object:
a = {};                       // assign to value-initialized state β€” now definitely empty

The std::string case is particularly educational. Small-string optimization (SSO) means a short string may store characters inline rather than on the heap. After a move from an SSO string, the moved-from object may retain the original characters or be empty β€” both are valid implementations. Never depend on which.

Defensive self-move-assignment

The standard guarantees library types handle self-move-assignment (v = std::move(v)) without undefined behavior since C++11. Prior to C++11 some containers had UB. For types you write, the explicit self-check or the copy-and-swap idiom handles it:

cpp
// Explicit self-check approach (used in the Buffer example above)
Buffer& operator=(Buffer&& other) noexcept {
    if (this == &other) return *this;  // guard against v = std::move(v)
    delete[] data_;
    data_ = std::exchange(other.data_, nullptr);
    size_ = std::exchange(other.size_, 0);
    return *this;
}

// Copy-and-swap approach β€” naturally handles self-assignment at the cost of
// an extra allocation for the copy path
Buffer& operator=(Buffer other) noexcept {  // takes by value β€” copy or move
    std::swap(data_, other.data_);
    std::swap(size_, other.size_);
    return *this;   // other (now holding old data_) is destroyed here
}

Best Practice. After move-assigning from a variable you still need to use, immediately assign it to a known good state:

cpp
std::vector<int> src = {1, 2, 3};
std::vector<int> dst = std::move(src);
src = {};           // explicitly reset to empty β€” now safe to use src again

Self-Move-Assignment

v = std::move(v) (assigning an object to itself via move) is defined behavior for all standard library types since C++11. The result is a "valid but unspecified" state β€” the object survives, but its value is not guaranteed to be unchanged.

For user-defined types the correct approaches are:

cpp
// Option 1: explicit identity check (cheapest for types with pointer members)
Widget& operator=(Widget&& other) noexcept {
    if (this == &other) return *this;
    // ... transfer members ...
    return *this;
}

// Option 2: copy-and-swap (unified assignment β€” handles both copy and move)
// Passing by value means the compiler selects copy ctor or move ctor depending
// on the call site. Self-move passes through the move ctor β€” creating a second
// object, so this != &other is always true inside the body.
Widget& operator=(Widget other) noexcept {
    std::swap(member1_, other.member1_);
    std::swap(member2_, other.member2_);
    return *this;
}

The copy-and-swap idiom naturally avoids the self-assignment problem without a branch, at the cost of one extra construction for the copy path. For move assignment specifically, the "by value" parameter is move-constructed from other, so no extra allocation occurs.

Common Pitfall. Writing delete[] data_; data_ = other.data_; other.data_ = nullptr; without a self-check frees the memory and then reads the now-dangling pointer. This is the classic use-after-free bug hiding in move assignment.


std::move on Non-Movable and const Types

std::move is a cast to T&&. Overload resolution then selects the best matching constructor or assignment operator. If the target type has no move constructor β€” or if the source is const β€” the compiler silently falls back to the copy constructor. No error or warning is emitted by default.

const objects always copy

A move constructor takes T&& (non-const). A const object produces const T&&, which does not bind to T&&. The compiler selects const T& (the copy constructor) instead:

cpp
const std::string s = "hello";
std::string t = std::move(s);   // compiles β€” but COPIES, not moves
                                // std::move produces const std::string&&
                                // no move ctor matches const T&&
                                // copy ctor (const T&) is selected silently

The silent fallback means std::move on a const object is always a no-op from a performance standpoint. It neither causes UB nor modifies s β€” it just does nothing useful.

Move-deleted or move-absent types

A type with no move constructor (either absent or = delete) also falls back to copy when moved:

cpp
struct CopyOnly {
    CopyOnly(const CopyOnly&) = default;  // copy exists
    CopyOnly(CopyOnly&&)      = delete;   // move explicitly deleted
};

CopyOnly a;
CopyOnly b = std::move(a);   // ERROR: move ctor is deleted, copy ctor not chosen
                             // because a deleted function is found first in overload resolution

Note the asymmetry: a missing move constructor falls back to copy, but an explicitly deleted one does not. Deletion makes the function visible to overload resolution and immediately causes an error.

Detecting silent copy fallback

Use static_assert to catch types that will copy where you expected a move:

cpp
template<typename T>
void move_into_container(std::vector<T>& v, T&& item) {
    // C++17: verify we will actually move, not copy
    static_assert(std::is_nothrow_move_constructible_v<T>,
        "T must have a noexcept move constructor; otherwise vector reallocates by copying");
    v.push_back(std::move(item));
}

Return Value and Implicit Moves

The C++ standard has progressively widened the cases where the compiler automatically applies move semantics on return, removing the need for explicit std::move.

C++11: named local variables

Returning a named local variable of the same type as the function's return type triggers an implicit move attempt:

cpp
// C++11: compiler first attempts move, falls back to copy if no move ctor
std::string make_greeting(const char* name) {
    std::string result = "Hello, ";
    result += name;
    return result;   // implicit move β€” no std::move needed
}

C++17: NRVO elides entirely

When NRVO applies, neither copy nor move occurs β€” the named local is the return value. Explicit std::move defeats NRVO:

cpp
// C++17 with NRVO β€” zero copies and zero moves
Widget make_widget() {
    Widget w{42};
    w.configure();
    return w;           // NRVO fires: w is directly constructed in caller's frame
}

// WRONG: std::move prevents NRVO β€” forces a move where elision was better
Widget make_widget_bad() {
    Widget w{42};
    w.configure();
    return std::move(w);   // NRVO suppressed; one move happens unnecessarily
}

C++20 (P1825): rvalue reference parameters and throw

C++20 extended implicit move to cover rvalue reference parameters and throw operands:

cpp
// C++20: implicit move applies to rvalue reference parameters on return
std::string process(std::string&& s) {
    // ...
    return s;           // C++20: implicit move β€” no std::move needed
    // (Before C++20, this would copy because s is an lvalue inside the body)
}

// C++20: implicit move on throw
void maybe_throw(std::string s) {
    if (error) throw s;  // C++20: implicit move; before C++20 this copied
}

C++23 (P2266): broader implicit move

C++23 removed the "two-overload-resolution-pass" restriction. The compiler now directly applies move for a wider set of return expressions, including cases that C++20 would have fallen back to copy on.

When explicit std::move on return is needed

There are two legitimate cases:

cpp
// Case 1: returning a member, not a local β€” NRVO cannot apply
class Builder {
    std::vector<int> data_;
public:
    std::vector<int> release() {
        return std::move(data_);  // correct: data_ is a member, not a local
    }
};

// Case 2: return type differs from the local's type β€” implicit move may not fire
Widget downcast(DerivedWidget w) {
    return std::move(w);   // explicit move needed: return type Widget != DerivedWidget
                           // (though std::move is still usually wrong here β€” rethink design)
}

Best Practice. The rule is simple: never write return std::move(local_variable) when the local is of the same type as the return type. For any other case β€” returning a member, returning a different type β€” evaluate whether explicit std::move is actually needed.


Structured Bindings and Move (C++17)

Structured bindings (auto [a, b] = expr) work with move semantics but the behavior differs from what it looks like.

What actually happens

A structured binding declaration creates a hidden anonymous entity initialized from expr. The names a and b are references into that anonymous entity, not independent objects.

cpp
std::pair<std::string, int> make_pair_val();

// C++17: auto [name, count] = ...
// The anonymous entity is copy-constructed or move-constructed from expr.
// name and count are references into the anonymous entity, not new objects.

auto [name, count] = make_pair_val();   // anonymous pair is move-constructed (prvalue)
                                         // name and count refer into it

std::pair<std::string, int> p{"hello", 42};
auto [n2, c2] = std::move(p);           // anonymous pair is move-constructed from p
                                         // p.first and p.second are now moved-from

The key insight: std::move(p) in a structured binding initializer moves the pair as a whole. The individual members p.first and p.second are moved out of p into the anonymous entity. After this line, p is in a moved-from state.

cpp
std::pair<std::string, int> p{"hello", 42};
auto [s, n] = std::move(p);   // s refers to the string that was moved from p.first
                               // n refers to the int that was moved from p.second
                               // p is now moved-from: p.first may be empty

// s and n are bound references β€” their lifetime is the anonymous entity's lifetime
// They are not independent std::string or int objects

Reference bindings do not move

cpp
std::pair<std::string, int> p{"hello", 42};

auto& [s, n] = p;               // no move, no copy β€” s and n are refs into p directly
auto&& [s2, n2] = std::move(p); // anonymous entity is ref-bound to p (rvalue ref)
                                 // no copy, no move of p itself here
                                 // but s2/n2 are xvalue refs β€” moving from them is valid

Common Pitfall. auto [a, b] = std::move(pair) is often used expecting a and b to be independent moved-from copies. They are references into an anonymous object. If you need genuinely independent variables, extract them:

cpp
auto [a_ref, b_ref] = std::move(p);          // a_ref and b_ref are bound references
std::string a = std::move(a_ref);             // now a is an independent object
int         b = b_ref;                        // b is a copy of the int

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.
  • Reset moved-from objects before reuse: the standard guarantees only "valid but unspecified". If you need to reuse a moved-from object, assign a known good state first.
  • Use static_assert(std::is_nothrow_move_constructible_v<T>) in performance-critical generic code to catch types that will silently degrade to copying.

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.

std::move on a const object silently copies

cpp
const std::string s = "expensive data";
std::string t = std::move(s);   // copies β€” std::move on const produces const T&&
                                 // which binds to the copy constructor, not the move ctor
                                 // s is unchanged; t is a copy

There is no compiler warning for this in most configurations. The copy happens silently and efficiently, but the intent was presumably to move. If you need to move from a logically const context, the design requires revisiting.


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
  • Rule of Five β€” compiler generation rules for all five special member functions
  • Copy Elision β€” when the compiler elides copies and moves entirely
  • Value Categories β€” lvalue, prvalue, xvalue: the formal taxonomy
  • Type Erasure β€” move-only type-erased wrappers (e.g., std::move_only_function, C++23)