C++ • C++-Specific Topics

Using std::move — Use std::move appropriately (intro)

Unlock efficient resource transfer by understanding when and why to cast lvalues to rvalue references.

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, transferring ownership of a resource — a heap-allocated buffer inside a std::string, for instance — invariably required a deep copy. The language provided no standard mechanism to express the notion that an object was about to be discarded and its internals could safely be pilfered. Library authors resorted to ad-hoc techniques such as swap idioms and auto_ptr — a smart pointer whose copy semantics were actually transfer semantics, creating endless confusion and subtle bugs. The core issue was a mismatch between what the programmer intended ("I am done with this object; please take its guts") and what the type system could express.

2002 • Howard Hinnant's Move Proposal

Howard Hinnant, Peter Dimov, and Dave Abrahams publish the seminal proposal N1377, introducing rvalue references and the concept of move semantics to the C++ standards committee.

2006 • Rvalue References Refined

After extensive debate, N2027 refines the specification. The distinction between T&& (rvalue reference) and T& (lvalue reference) becomes a first-class part of the type system.

2011 • C++11 Ratified

The ISO publishes C++11. Move constructors, move assignment operators, and std::move become standard. auto_ptr is deprecated in favor of std::unique_ptr.

2017 • Guaranteed Copy Elision (C++17)

C++17 mandates copy elision in certain contexts (e.g., returning a prvalue), reducing cases where an explicit std::move is needed — or where it actually hurts performance by inhibiting elision.

The central question that move semantics answers is deceptively simple: how do we let the compiler distinguish objects that are about to expire from those that must persist? The answer lies in value categories, rvalue references, and a small utility function called std::move — which, as we shall see, does not actually move anything.

SECTION 2

Core Principles & Definitions

To use std::move correctly, you need a firm grasp of the building blocks that underpin move semantics. The concepts below form a dependency chain: value categories define the language rules, rvalue references give the type system a hook, and std::move provides a convenient cast that connects intention to mechanism.

1

Value Categories

Every expression in C++ is either an lvalue (has a persistent address) or an rvalue (a temporary whose resources may be stolen). C++11 further subdivides rvalues into prvalues and xvalues (eXpiring values).
2

Rvalue References (T&&)

An rvalue reference is a reference type that can bind only to rvalues. It enables overload resolution to select a move constructor or move assignment operator instead of their copy counterparts.
3

std::move Is Just a Cast

std::move(x) performs an unconditional cast of x to an rvalue reference. It generates zero machine instructions. The actual resource transfer happens when the resulting rvalue reference binds to a move constructor or move assignment operator.
4

Moved-From State

After a move, the source object is in a valid but unspecified state. You may destroy it or assign a new value to it, but you must not rely on its previous contents.
5

Copy Elision & NRVO

The compiler may eliminate copies and moves entirely through Named Return Value Optimization (NRVO). Using std::move on a return expression can actually inhibit this optimization — a critical pitfall.
✦

KEY TAKEAWAY

Think of std::move like signing a release form on a shipping crate. The form itself doesn't transport anything — it merely declares, "I no longer need this crate; anyone who accepts it may take the contents." The actual moving of goods happens when the recipient (a move constructor or move assignment operator) processes the release. If nobody accepts the form, the crate stays untouched.

SECTION 3

Visual Explanation — How std::move Triggers a Move

Overload Resolution: Copy vs. Movesrc (lvalue)heap buffer: [H][e][l][l][o]size: 5, capacity: 8std::move(src)static_cast<string&&>(src)Zero runtime costlvalueCopy Constructorstring(const string&)Allocates new buffer,copies every byteNOT selected ✗Move Constructorstring(string&& other)Steals pointer, setsother to emptySELECTED ✓rvalue refdst (new object)heap buffer: [H][e][l][l][o]stolen from src — O(1)src (moved-from)heap buffer: nullptrvalid but unspecified statepointer ownership transferred
The diagram traces what happens when string dst = std::move(src); executes. The cast produced by std::move (center box) yields an rvalue reference, steering overload resolution to the move constructor instead of the copy constructor. The move constructor then steals the heap buffer pointer in O(1) time, leaving src in a valid-but-unspecified (typically empty) state.

Notice that std::move itself appears as a pure compile-time cast — there is no runtime branch, no allocation, and no memcpy. The efficiency gain comes entirely from the move constructor that the cast enables. If a type lacks a move constructor (for example, a class with a user-declared destructor and no explicitly defined move operations), the compiler will fall back to the copy constructor, and std::move effectively does nothing beneficial. Understanding this two-step process — cast, then bind — is the single most important insight for using std::move appropriately.

SECTION 4

Under the Hood — The Implementation of std::move

The implementation of std::move in the standard library is remarkably short. Conceptually, it boils down to a single static_cast. Let us look at the canonical formulation and understand each piece.

SIMPLIFIED DEFINITION OF std::move

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

T&& — a forwarding (universal) reference when T is deduced; remove_reference_t<T> strips any reference qualifier so the result is always a pure rvalue reference; noexcept — guaranteed not to throw, which is essential for strong exception safety in containers.

Reference Collapsing Rules

When std::move is called with an lvalue of type std::string, template argument deduction yields T = std::string&. The reference collapsing rules state that T& && collapses to T& (so the parameter binds the lvalue), while remove_reference_t<T>&& produces std::string&& — a clean rvalue reference. When called with an rvalue, T = std::string directly, and the same result type emerges.

ExpressionDeduced TParameter type (collapsed)Return type
std::move(lvalue)string&string& && → string&string&&
std::move(rvalue)stringstring&&string&&
std::move(const lval)const string&const string&const string&&

Reference collapsing for different argument categories

⚠

The const Trap

Calling std::move on a const object yields a const T&&. No move constructor takes a const T&& — the copy constructor (const T&) will be selected instead. The code compiles silently, but you get a copy, not a move. This is one of the most common std::move misuses.

SECTION 5

When to Use — and When NOT to Use — std::move

Knowing the mechanics is necessary but not sufficient. The real skill lies in recognizing the right contexts for std::move and, equally important, the contexts where it is harmful or useless. The following classification covers the most common scenarios you will encounter.

Decision Flowchart: Should You Use std::move?Do you own the object?NOYES❌ Do NOT move — not yours to giveWill you use the objectagain after this line?YESNO❌ Do NOT move — you still need itIs it a return statementof a local variable?YESNO⚠️ Usually do NOT move —NRVO is likely better (C++17+)Is the objectconst?YESNO❌ Move on const = silent copy✅ Move!std::move is correct
Follow this decision tree whenever you consider using std::move. The green ✅ endpoint indicates the safe scenario: you own the object, you will not use it again, it is not a simple return of a local, and it is non-const. Every other path leads to a red ❌ (harmful) or amber ⚠️ (likely counterproductive) outcome.

Common Appropriate Uses

  • ✓ Sink parameters — when a function takes a value it intends to own (e.g., void setName(std::string name)), the caller can std::move its string to avoid a copy.
  • ✓ Inserting into containers — vec.push_back(std::move(temp)) when you no longer need temp.
  • ✓ Transferring unique ownership — std::unique_ptr requires std::move for ownership transfer since it has no copy operations.
  • ✓ Last use of a local before it goes out of scope (in non-return contexts) — e.g., passing a local to a constructor inside a loop body.

Common Misuses

  • Moving a const object — silently degrades to a copy.
  • Moving a return value — return std::move(local); prevents NRVO; omit the std::move and let the compiler elide.
  • Moving trivially-copyable types (e.g., int, double) — moving a primitive is identical to copying; std::move adds noise for zero benefit.
  • Using the object after moving — accessing a moved-from object (other than to destroy or reassign it) is a logic error.

SECTION 6

Worked Example — Building a Message Queue

Consider a simple message queue that stores strings. We will trace through the code, identifying each opportunity to use std::move and explaining the performance consequences.

Moving Strings Into a Queue

  1. 1

    Step 1 — Define the Sink Function

    We write void MessageQueue::enqueue(std::string msg). The parameter msg is taken by value — a common "sink" idiom. Taking by value means the caller decides whether to copy or move, and the function always owns a fresh object.
    void enqueue(std::string msg); — by-value sink parameter.
  2. 2

    Step 2 — Move Into the Container

    Inside enqueue, msg is a named local — an lvalue. If we write queue_.push_back(msg), we copy. Since msg is about to go out of scope and we do not need it again, we write: queue_.push_back(std::move(msg));.
    One move instead of one copy. For a 1 MB string, this turns an O(n) allocation + memcpy into an O(1) pointer swap.
  3. 3

    Step 3 — Call Site with an Lvalue

    At the call site: std::string payload = buildPayload(); followed by mq.enqueue(std::move(payload));. Here std::move(payload) casts the lvalue to an rvalue reference, which initializes the by-value parameter msg via the move constructor of std::string. After this line, payload is moved-from — do not read from it.
    Total cost: 2 moves (call site → parameter, parameter → container), 0 copies.
  4. 4

    Step 4 — Call Site with a Temporary

    Alternatively: mq.enqueue(buildPayload());. The return value of buildPayload() is already a prvalue, so no std::move is needed at the call site — the by-value parameter is initialized directly via move (or elision in C++17+).
    Total cost: 1 move (parameter → container); the first move may be elided entirely.
  5. 5

    Step 5 — Verify the Moved-From Object

    If after Step 3 you accidentally write std::cout << payload;, you read from a moved-from string. For std::string, the Standard guarantees it is in a valid state (likely empty), so this won't crash — but the output is meaningless. Static analysis tools like clang-tidy (bugprone-use-after-move) can catch this mistake at compile time.
    Rule of thumb: treat any moved-from object as dead. Assign or destroy — never inspect.

SECTION 7

Copy vs. Move vs. Elision — A Comparison

It is helpful to compare the three resource-transfer strategies that modern C++ supports side by side. Each has a different cost profile, a different set of preconditions, and different implications for code clarity.

CriterionDeep CopyMoveCopy Elision (NRVO)
Runtime costO(n) — allocates new memory, copies every byteO(1) — pointer/handle swap; source left emptyZero — object is constructed directly in the destination
Source object afterUnchanged — still fully usableValid but unspecified; do not readN/A — no separate source exists
Requires explicit std::move?No — default for lvaluesYes (for lvalues)No — compiler applies automatically when conditions are met
Can std::move inhibit it?NoN/AYes — adding std::move to a return statement disables NRVO
Works with const objects?YesSilently falls back to copyYes

Comparison of resource-transfer strategies in modern C++

✦

KEY TAKEAWAY

Think of these three strategies as tiers of efficiency for shipping a package. A deep copy is like photocopying every page of a book and mailing the duplicate. A move is like handing the original book to the recipient and erasing your bookmark — no duplication, but the original is gone from your shelf. Copy elision is like writing the book directly onto the recipient's shelf — the book was never on your shelf at all. Prefer elision when the compiler can do it; use std::move when it cannot.

SECTION 8

Connection to Perfect Forwarding & Advanced Move Semantics

While std::move provides an unconditional cast to rvalue reference, its close sibling std::forward provides a conditional cast — it preserves the original value category of the argument. This distinction is crucial in template code that must relay arguments to another function without losing their rvalue-ness. Together, std::move and std::forward form the backbone of efficient generic programming in modern C++.

Featurestd::movestd::forward<T>
Cast typeUnconditional — always yields T&&Conditional — yields T& if lvalue, T&& if rvalue
Use caseTransferring ownership from a named object you no longer needRelaying arguments in templates (perfect forwarding)
Requires template param?No — works with any expressionYes — must be called as std::forward<T>(arg)
Common mistakeUsing on const objects; using on return localsUsing std::move where std::forward is needed in generic code

std::move vs. std::forward

Looking further ahead, C++23 introduces deducing this (explicit object parameters), which simplifies many patterns that previously required separate lvalue and rvalue overloads. The std::move utility remains relevant in all these contexts, but the language is steadily evolving to reduce the number of places where the programmer must intervene manually. A thorough understanding of std::move lays the groundwork for mastering these advanced features.

SECTION 9

Practice Problems

Question 1: CONCEPTUAL

Explain why std::move is said to "not move anything." What does it actually do, and what is responsible for the actual resource transfer?
Show answer
std::move performs a static_cast to an rvalue reference (T&&). It generates no machine instructions at runtime; it merely changes the value category of the expression from lvalue to xvalue. The actual resource transfer occurs in the move constructor or move assignment operator of the type, which overload resolution selects because the argument is now an rvalue. Without a move-aware type, the cast has no effect.

Question 2: BASIC CALCULATION

Given the code: std::vector<int> a = {1, 2, 3, 4, 5}; std::vector<int> b = std::move(a); After these two lines, what are the guaranteed states of a and b? Is it safe to call a.size() afterward?
Show answer
b now owns the five elements {1, 2, 3, 4, 5}. a is in a valid but unspecified state — the Standard does not guarantee it is empty, but in practice all major implementations leave it empty. Calling a.size() is safe because size() is a basic operation permitted on any valid object. However, you should not rely on the returned value being 0 or any other specific number unless you first assign to or clear a.

Question 3: INTERMEDIATE

Identify the bug in the following code: std::string buildGreeting(const std::string& name) { std::string result = "Hello, " + name + "!"; return std::move(result); } What would you change, and why?
Show answer
The std::move(result) on the return statement is counterproductive. Without it, the compiler would apply Named Return Value Optimization (NRVO), constructing result directly in the caller's return slot — zero copies and zero moves. The explicit std::move inhibits NRVO because the return expression's type no longer matches the function's return type in the way required by the elision rules. The fix is simply: return result;. Even if NRVO does not fire, the compiler is required by C++11 to treat a returned local as an rvalue implicitly.

Question 4: APPLIED

You are implementing a thread-safe logger. The log method must accept a string message, lock a mutex, and append the message to an internal buffer. Design the function signature and show how std::move should be used both in the method body and at the call site. Explain each move.
Show answer
Signature: void Logger::log(std::string msg) — take by value (sink). Body: std::lock_guard<std::mutex> lk(mtx_); buffer_.push_back(std::move(msg));. Inside the body, msg is an lvalue about to go out of scope, so std::move is correct before push_back. At the call site with a local string s: logger.log(std::move(s)); moves into the by-value parameter. Total: 2 moves, 0 copies. For a temporary: logger.log("error"); — the temporary binds via move (or elision) automatically, so no explicit std::move is needed at the call site.

Question 5: CRITICAL THINKING

A colleague argues that every function parameter should be taken by rvalue reference (T&&) instead of by value to "maximize move efficiency." Construct a counter-argument. Consider both interface usability and the case where the caller wants to pass a copy that they still need.
Show answer
Taking by T&& forces every caller to use std::move — even callers who hold an lvalue they still need. Those callers must first make an explicit copy (auto temp = original; f(std::move(temp));), which is awkward and error-prone. A by-value parameter (void f(T x)) lets callers who own the object std::move into it, while callers who need to retain the original simply pass normally — the copy happens at the call site, neatly expressed in one line. The by-value idiom costs at most one additional move (parameter initialization), which is O(1) for types with cheap moves. For types where even one extra move is unacceptable, the better solution is overloading on const T& and T&&, not forcing rvalue-only parameters. Interface usability and flexibility outweigh the micro-optimization of avoiding a single cheap move.

SUMMARY

Summary — Using std::move Appropriately

Summary

std::move is a compile-time cast that converts an lvalue into an rvalue reference (T&&), enabling overload resolution to select a move constructor or move assignment operator instead of a copy. It generates zero machine instructions on its own; the actual resource transfer is performed by the type's move operations, typically in O(1) time by swapping internal pointers. After a move, the source object is in a valid but unspecified state — you may destroy or reassign it, but you must not read its previous value.

Use std::move when you own an object, you no longer need it, and copying would be wasteful — common scenarios include sink parameters, container insertions, and unique_ptr transfers. Avoid it on const objects (silent fallback to copy), on return statements of local variables (inhibits NRVO), and on trivially-copyable types where it adds noise for zero benefit. As you advance, the conditional counterpart std::forward will extend these ideas into the realm of perfect forwarding in generic template code.

Varsity Tutors • C++ • Using std::move — Use std::move appropriately (intro)