C++ • C++-Specific Topics

Move Semantics — Understand move constructor and move assignment conceptually (intro)

Eliminate unnecessary copies by transferring ownership of resources, unlocking C++11's most impactful performance optimization.

Programming braces with a command terminal and binary digits.
Inside this lesson 10 sections 5 questions 6 figures

SECTION 1

Historical Context & Motivation

Before C++11, every time you returned a large object from a function, inserted an element into a std::vector, or passed a temporary to a constructor, the language had no choice but to invoke a copy constructor or copy assignment operator. This meant deep-copying heap-allocated buffers, duplicating file handles, and cloning any other resources the object managed—even when the source object was a nameless temporary about to be destroyed on the very next line. The performance cost was enormous in resource-heavy code paths, and experienced C++ developers went to considerable lengths to work around it: returning raw pointers, passing output parameters by reference, or relying on compiler-specific optimizations like Return Value Optimization (RVO) that were not guaranteed by the standard.

The core insight that eventually led to move semantics was simple: if an object is about to die, there is no reason to copy its guts—we can just steal its resources instead. This observation had been discussed in the C++ community since the late 1990s, but formalizing it required new language machinery: rvalue references, the std::move utility, and clearly defined rules for when the compiler may treat an object as "moveable."

1998 • C++98 Standard Published

The first ISO C++ standard codifies copy constructors and copy assignment as the primary mechanisms for transferring object state. No move-specific language feature exists; every value transfer is a deep copy unless the compiler applies optional optimizations.

2002 • Howard Hinnant's Move Proposal

Howard Hinnant, along with Peter Dimov and Dave Abrahams, drafts early proposals (N1377) introducing the idea of rvalue references and move constructors. The proposal demonstrates order-of-magnitude speedups for standard containers.

2006 • Rvalue References Refined

After multiple revisions (N1690, N1770, N2118), the rvalue reference syntax T&& is stabilized. The committee agrees on the distinction between lvalues and rvalues as the mechanism for triggering move operations.

2011 • C++11 Ratified

Move semantics become part of the official language. The standard library is overhauled: std::vector, std::string, std::unique_ptr, and other types now support move constructors and move assignment operators, delivering dramatic performance gains.

2017–2020 • Guaranteed Copy Elision & Refinements

C++17 guarantees copy elision in certain contexts (prvalues), and C++20 further refines move semantics with concepts, constexpr destructors, and improved template argument deduction involving rvalue references.

The fundamental question move semantics answers is: When an object's lifetime is ending, how can we transfer its expensive resources to a new owner without paying for a full copy? Understanding the answer to this question is essential for writing efficient modern C++ and for grasping how the standard library containers achieve their performance characteristics.

SECTION 2

Core Principles & Definitions

Move semantics rest on a handful of interconnected ideas that together change the way C++ programs manage object lifetimes and resource ownership. Before diving into syntax, it is important to internalize the conceptual vocabulary that underpins the entire mechanism.

1

Lvalues vs. Rvalues

An lvalue is an expression with a persistent identity—you can take its address (e.g., a named variable). An rvalue is a temporary without persistent identity (e.g., the return value of a function or a literal). Move semantics exploit the fact that rvalues are about to be destroyed anyway.
2

Rvalue References (T&&)

The syntax T&& declares an rvalue reference—a reference type that binds exclusively to rvalues. This new reference category is the language hook that lets the compiler dispatch to a move constructor instead of a copy constructor when the source is a temporary.
3

Move Constructor

A move constructor has the signature T(T&& other). Instead of deep-copying other's resources, it transfers ownership—typically by copying pointer values and then nullifying the source. The result is O(1) for pointer-based resources.
4

Move Assignment Operator

The move assignment operator has the signature T& operator=(T&& other). It releases the current object's resources, steals the source's resources, and leaves the source in a valid-but-unspecified state. Semantically parallel to the move constructor but for already-constructed objects.
5

std::move — A Cast, Not a Move

std::move(x) does not move anything. It is a static_cast to T&&, telling the compiler: "I no longer need the value of x; you may treat it as an rvalue and dispatch to a move constructor or move assignment operator."
✦

KEY TAKEAWAY

Think of move semantics like moving apartments. A copy is like buying brand-new, identical furniture for the new apartment and leaving the old furniture in the old apartment. A move is like loading the old furniture onto a truck and driving it to the new place—the old apartment is left empty but structurally intact. The furniture (heap data) is not duplicated; it is transferred. The old apartment (source object) still exists and can be torn down (destructed) safely, but it no longer contains anything useful.

SECTION 3

Visual Explanation — Copy vs. Move

The following diagram contrasts what happens in memory during a copy construction versus a move construction for a simple class that owns a dynamically allocated array. In the copy path, the entire heap buffer is duplicated; in the move path, only a pointer and a size integer are transferred, and the source is left in a hollow state.

Copy Construction vs. Move ConstructionCOPY PATHMOVE PATHsrc (lvalue)data_: 0xA100size_: 1000Heap 0xA100[1000 ints]copy ctor duplicates heapsrc (unchanged)data_: 0xA100size_: 1000Heap 0xA100[1000 ints]dst (new copy)data_: 0xB200size_: 1000Heap 0xB200[1000 ints]Cost: O(n) — 1000 ints copiedsrc (rvalue)data_: 0xA100size_: 1000Heap 0xA100[1000 ints]move ctor steals pointersrc (hollowed)data_: nullptrsize_: 0dst (new owner)data_: 0xA100size_: 1000Heap 0xA100[1000 ints]Cost: O(1) — pointer swap onlyTwo separate heap allocations existOnly one heap allocation — ownership transferred
Left: the copy path allocates a second heap buffer at 0xB200 and element-wise copies all 1 000 integers. Right: the move path simply transfers the pointer 0xA100 to the destination and nullifies the source, resulting in constant-time transfer regardless of buffer size.

Notice how, on the right side, the source object after the move is in a valid but unspecified state: its pointer has been set to nullptr and its size to zero. This is a critical invariant—the source's destructor must still run safely. Because the pointer is null, delete[] nullptr is a well-defined no-op in C++, so the destruction of the hollowed source is harmless. Meanwhile, the destination now exclusively owns the buffer. The total number of heap allocations drops from two (copy) to one (move), and no element-level work is performed.

SECTION 4

How It Works — Overload Resolution & Value Categories

Move semantics work through C++'s existing function overload resolution mechanism. When both a copy constructor T(const T&) and a move constructor T(T&&) are available, the compiler selects the best match based on the value category of the argument. An lvalue binds to const T&, triggering a copy; an rvalue binds to T&&, triggering a move. This is not a special-case rule—it follows directly from overload resolution and reference binding rules.

Value Categories at a Glance

CategoryHas Identity?Movable?Example
lvalueYesNo (unless cast)int x = 42;
xvalueYesYesstd::move(x)
prvalueNoYesmakeWidget()

C++11 value categories (simplified). xvalues and prvalues are collectively called rvalues.

An xvalue ("expiring value") is an expression that has identity but has been explicitly marked as moveable via std::move or returned from a function as T&&. A prvalue ("pure rvalue") is a temporary that never had a name—like the result of Widget() or 3 + 4. Both are rvalues, so both bind to T&& parameters and trigger move operations.

Dispatch Logic

OVERLOAD SELECTION RULE

f(lvalue) → f(const T&) // copy f(rvalue) → f(T&&) // move

If no move overload exists, the compiler falls back to the copy overload because const T& can bind to both lvalues and rvalues. This provides backward compatibility: C++98 code continues to compile and run correctly without modification.

⚠

std::move Does Not Move

This is a common source of confusion. std::move(x) is equivalent to static_cast<T&&>(x). It does not move anything—it merely produces an xvalue so that overload resolution selects the move constructor or move assignment operator. The actual transfer of resources happens inside those special member functions, not inside std::move.

SECTION 5

Anatomy of a Move-Aware Class

To make a class "move-aware," you implement both a move constructor and a move assignment operator alongside the traditional copy constructor, copy assignment operator, and destructor—collectively known as the Rule of Five. The diagram below shows the internal structure of a class Buffer that manages a dynamically allocated array and implements all five special member functions. Pay close attention to how the move operations differ from their copy counterparts.

Rule of Five — Buffer Class Special Member Functionsclass Buffer { int* data_; size_t size_; };COPY CONSTRUCTORBuffer(const Buffer& o) : size_(o.size_) , data_(new int[o.size_]){ std::copy(...); }O(n) — allocates + copiesMOVE CONSTRUCTORBuffer(Buffer&& o) noexcept : data_(o.data_) , size_(o.size_){ o.data_=nullptr; o.size_=0; }O(1) — steals pointerDESTRUCTOR~Buffer(){ delete[] data_; }Safe even when data_== nullptr (no-op)COPY ASSIGNMENT OPERATORBuffer& operator=(const Buffer& o) { if (this != &o) { delete[] data_; size_ = o.size_; data_ = new int[size_]; std::copy(...); } return *this; }MOVE ASSIGNMENT OPERATORBuffer& operator=(Buffer&& o) noexcept { if (this != &o) { delete[] data_; data_ = o.data_; size_ = o.size_; o.data_=nullptr; o.size_=0; } return *this; }Key Pattern in Move Operations1. Copy the source's pointer/size into *this → 2. Null out the source's pointer; zero the source's sizeThis two-step "steal-and-nullify" idiom ensures exactly one owner and a safe source destructor.Copy ops: allocate + deep copy → O(n)Move ops: pointer swap + nullify → O(1)
The five special member functions of a move-aware Buffer class. The green boxes (move constructor, move assignment) perform O(1) pointer transfers, while the pink/violet boxes (copy constructor, copy assignment) perform O(n) deep copies. The destructor safely handles both moved-from (null pointer) and normal states.

Several observations are worth emphasizing. First, both move operations are marked noexcept. This is not merely stylistic—the standard library's std::vector will only use your move constructor during reallocation if it is noexcept; otherwise it falls back to copying to preserve the strong exception safety guarantee. Second, the pattern inside every move operation is identical: steal the resource, then nullify the source. Third, the self-assignment check (this != &o) is technically optional for move operations in many implementations, but including it is defensive and costs almost nothing.

💡

The noexcept Contract

Marking move operations noexcept is critical for standard-library performance. Containers like std::vector check std::is_nothrow_move_constructible<T>::value at compile time. If it returns false, the vector copies during reallocation instead of moving—entirely negating the benefit. Always declare move constructors and move assignment operators noexcept unless they genuinely can throw.

SECTION 6

Worked Example — Tracing a Move Through Code

Consider the following program that uses the Buffer class described above. We will trace which special member functions are invoked and what happens to the underlying pointers at each step.

Tracing Move Operations in Buffer

  1. 1

    Step 1 — Construct the original object

    We write Buffer a(1000); which calls a parameterized constructor. Internally, a.data_ is set to the address returned by new int[1000] — let us say 0xA100 — and a.size_ is set to 1000.
    a → { data_: 0xA100, size_: 1000 }
  2. 2

    Step 2 — Move-construct a new object

    We write Buffer b(std::move(a));. The expression std::move(a) casts a to an rvalue reference (Buffer&&), so overload resolution selects the move constructor. Inside the move constructor, b.data_ is initialized to a.data_ (i.e., 0xA100), b.size_ to 1000, and then a.data_ is set to nullptr and a.size_ to 0. No heap allocation occurs.
    b → { data_: 0xA100, size_: 1000 }, a → { data_: nullptr, size_: 0 }
  3. 3

    Step 3 — Create another object and move-assign

    We write Buffer c(500); allocating a separate buffer at, say, 0xC300. Then c = std::move(b); invokes the move assignment operator. Inside, c first frees its own buffer (delete[] 0xC300), then steals b's pointer and size, and finally nullifies b.
    c → { data_: 0xA100, size_: 1000 }, b → { data_: nullptr, size_: 0 }
  4. 4

    Step 4 — Destructors run at scope exit

    When the scope ends, destructors run in reverse order of construction. c's destructor calls delete[] 0xA100 — this frees the original heap buffer. b's destructor calls delete[] nullptr — a safe no-op. a's destructor also calls delete[] nullptr — another no-op. Exactly one deallocation occurs for the one allocation, with no double-frees or leaks.
    Heap summary: 2 allocations (0xA100 and 0xC300), 2 deallocations — no leaks, no double-frees.

SECTION 7

Comparing Copy and Move Semantics

The following table contrasts copy and move operations across several dimensions. Understanding these trade-offs is essential for deciding when to rely on implicit moves, when to use std::move explicitly, and when copying is actually the safer or necessary choice.

DimensionCopy SemanticsMove Semantics
ComplexityO(n) — proportional to resource sizeO(1) — constant, regardless of resource size
Heap allocationsOne new allocation per copyZero new allocations
Source state afterUnchanged — full independent cloneValid but unspecified (typically hollow)
Exception safetyMay throw (allocation failure)Typically noexcept
SignatureT(const T&) / operator=(const T&)T(T&&) / operator=(T&&)
Use caseNeed two independent objects with identical stateSource is temporary or no longer needed

Copy vs. Move: a side-by-side comparison

✦

KEY TAKEAWAY

Move semantics do not replace copy semantics—they complement them. Copies create independent clones suitable for parallel ownership; moves perform ownership transfers suitable for pipeline-style handoffs. A well-designed class supports both so the compiler can automatically choose the cheapest path based on context. Think of copies as photocopying a document (both copies are complete and useful) versus moves as handing the original document to someone else (fast, but the sender's hands are now empty).

SECTION 8

Connection to Advanced Topics

This introductory lesson establishes the conceptual foundation. Several advanced topics build directly on move semantics, and having a mental roadmap of where these ideas lead will help you appreciate why the community invested so much effort into this language feature.

Introductory Concept (This Lesson)Advanced Extension
Move constructor / move assignmentPerfect forwarding — using T&& in template contexts (forwarding references) to preserve value category across function calls
Rvalue references (T&&)Universal references — when T&& appears in a deduced template context, it can bind to both lvalues and rvalues (reference collapsing rules)
std::move as a caststd::forward<T> — conditionally casts to rvalue only if the original argument was an rvalue, enabling lossless forwarding
Valid-but-unspecified source stateDestructive move proposals (P1144) — future standards may allow trivially relocatable types where the source is not required to remain valid at all
Rule of FiveRule of Zero — when all members are RAII types (e.g., std::unique_ptr, std::vector), the compiler-generated special members do the right thing automatically

Roadmap from introductory move semantics to advanced topics

Perhaps the most important practical takeaway for future study is the Rule of Zero: if you compose your class exclusively from types that already manage their own resources (smart pointers, standard containers, std::string), the compiler-generated copy and move operations are correct by default, and you need to write none of the five special member functions yourself. The Rule of Five from this lesson is the fallback for classes that manage raw resources directly—and understanding it is precisely what enables you to appreciate why the Rule of Zero is so powerful.

SECTION 9

Practice Problems

Question 1: CONCEPTUAL

Explain in your own words why a move constructor is typically O(1) while a copy constructor is O(n) for a class that manages a dynamically allocated array. What specific operations does each perform?
Show answer
A copy constructor must allocate a new heap buffer of the same size as the source and then copy every element from the source buffer to the new buffer, making it O(n) where n is the number of elements. A move constructor, by contrast, merely copies the pointer value and the size integer from the source into the destination (two machine-word copies, which is O(1)), and then nullifies the source's pointer. No new allocation is performed and no elements are iterated. The constant-time cost of a move is independent of how much data the pointer addresses.

Question 2: BASIC CALCULATION

A class Image manages a heap-allocated array of 4 × 10⁶ pixels (each pixel is 4 bytes). If a function returns an Image by value and the compiler uses a move (no copy elision), approximately how many bytes of data must be copied during the move? How many during a deep copy?
Show answer
During a move, only the metadata is copied: typically one pointer (8 bytes on a 64-bit system) and one size field (8 bytes), totaling approximately 16 bytes. During a deep copy, the entire pixel array is duplicated: 4 × 10⁶ pixels × 4 bytes/pixel = 16 × 10⁶ bytes = 16 MB, plus the 16 bytes of metadata, plus the cost of a new allocation. The move is therefore roughly one million times cheaper in terms of data movement. This dramatic difference explains why move semantics have such a profound impact on code involving large data structures.

Question 3: INTERMEDIATE

Consider the following code: Buffer a(100); Buffer b(a); Buffer c(std::move(a)); For each of the three lines, state which special member function is called (parameterized constructor, copy constructor, or move constructor) and describe the state of a after all three lines have executed.
Show answer
Line 1 calls the parameterized constructor, allocating a 100-element buffer. Line 2 calls the copy constructor because a is an lvalue; this allocates a new buffer and copies all 100 elements from a into b. After line 2, a is unchanged. Line 3 calls the move constructor because std::move(a) casts a to an rvalue reference; c steals a's pointer and nullifies a. After all three lines, a is in a valid-but-unspecified state (typically data_ == nullptr, size_ == 0), and b and c each own independent copies of the original buffer's data.

Question 4: APPLIED

You are profiling a function that builds a std::vector<std::string> of 100 000 strings, each 1 KB long. The function returns the vector by value. In a pre-C++11 compiler without RVO, the return would deep-copy the entire vector. Estimate the total bytes copied in the pre-C++11 scenario versus the C++11 move scenario. Assume the vector's internal array of std::string objects is also heap-allocated.
Show answer
In the pre-C++11 scenario, the return deep-copies the vector, which means: (a) allocating a new internal array for the returned vector (100 000 × sizeof(std::string) ≈ 100 000 × 32 = 3.2 MB), and (b) copy-constructing each string, which allocates 1 KB each (100 000 × 1 024 = ~100 MB). Total: approximately 103 MB of allocation and copying. In the C++11 move scenario, the vector's move constructor transfers three pointers (begin, end, capacity) and nullifies the source—roughly 24 bytes of work. No string objects are copied or moved because the entire internal array changes ownership as a single pointer transfer. The savings are roughly six orders of magnitude. In practice, C++17 guarantees copy elision for prvalues, so even the move may be elided, but the move fallback alone is transformative.

Question 5: CRITICAL THINKING

A student writes a move constructor that does not nullify the source's pointer: Buffer(Buffer&& o) noexcept : data_(o.data_), size_(o.size_) { /* source not nullified */ } Explain what goes wrong and under what conditions. Is the program guaranteed to crash? Why does the standard mandate that moved-from objects be in a "valid but unspecified state"?
Show answer
Without nullifying the source, both the source and destination hold the same pointer. When their destructors run, both will call delete[] data_ on the same address—a double-free, which is undefined behavior. It may crash immediately, silently corrupt the heap, or appear to work in debug builds and fail catastrophically in release. The program is not guaranteed to crash because UB includes "apparently correct" behavior. The standard's requirement that moved-from objects be in a valid but unspecified state exists precisely to prevent this: the source must be destructible and assignable without causing resource corruption. The two-step "steal-and-nullify" idiom ensures unique ownership of the heap resource at all times, preserving the RAII invariant that every resource has exactly one owner responsible for releasing it.

SUMMARY

Summary — Move Semantics at a Glance

Summary

Move semantics, introduced in C++11, allow an object's resources to be transferred rather than duplicated when the source is a temporary or has been explicitly cast to an rvalue reference via std::move. The move constructor (T(T&&)) and move assignment operator (T& operator=(T&&)) implement this transfer by copying lightweight metadata (pointers, sizes) and nullifying the source, achieving O(1) performance instead of the O(n) cost of a deep copy.

The key vocabulary includes lvalues (named, addressable expressions) and rvalues (temporaries, expiring values), which drive overload resolution between copy and move operations. Move operations should be marked noexcept to enable standard-library optimizations. The Rule of Five reminds us that if we define any one of the five special member functions, we should define all five. Looking ahead, the Rule of Zero recommends composing classes from self-managing types so that compiler-generated defaults handle everything correctly.

Varsity Tutors • C++ • Move Semantics — Understand move constructor and move assignment conceptually (intro)