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.

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.

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.

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.

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.

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

C++11 value categories (simplified). xvalues and prvalues are collectively called rvalues.
CategoryHas Identity?Movable?Example
lvalueYesNo (unless cast)int x = 42;
xvalueYesYesstd::move(x)
prvalueNoYesmakeWidget()

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.

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.

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.

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
Step 1 — Construct the original objectWe 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
Step 2 — Move-construct a new objectWe 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
Step 3 — Create another object and move-assignWe 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
Step 4 — Destructors run at scope exitWhen 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.

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.

Copy vs. Move: a side-by-side comparison
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
✦ 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).

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.

Roadmap from introductory move semantics to advanced topics
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

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.

Practice Problems

PROBLEM 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?
PROBLEM 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?
PROBLEM 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.
PROBLEM 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.
PROBLEM 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"?

Summary — Move Semantics at a Glance

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)