2Brute2Best
πŸ”₯0
M2.3

Lvalues, Rvalues, Move Semantics & The Rule of 5 / 0

Medium High-frequency interview topic Pro OnlyLvalue vs Rvaluestd::moveMove Ctor & Assignmentnoexcept & vectorRule of 5 vs 0
⚑ 15-Second Mental ModelM2.3 · Instant Intuition
Single-Read Mastery
The Real-World Physical Analogy (Move Semantics (Rule of 5))

Copying is building an identical second house brick-by-brick. Moving is handing someone your house keys and changing your own address to null (src.ptr = nullptr).

Rule to Remember: std::move does not move anything! It only casts a variable into an rvalue so the move constructor can steal its pointer.
3 Golden Facts to Remember
Fact #1
What is the difference between an lvalue and an rvalue in C++?

An lvalue refers to a memory location that has an identifiable address and a lifetime that extends beyond a single expression (e.g. named variables, dereferenced pointers, references). An rvalue is an expression that does not have an identifiable persistent address, representing a temporary object or value that is about to expire at the semicolon (e.g. literals, temporary return values, or values cast via std::move).

Fact #2
What does std::move actually do at the assembly / machine level?

At the machine level, std::move generates zero instructions. It is purely a compile-time static cast: static_cast<std::remove_reference_t<T>&&>(val). It turns an lvalue expression into an xvalue (an rvalue), allowing overload resolution to select functions that take T&& (move constructors and move assignments) rather than const T&.

Fact #3
Why does std::vector require noexcept on move constructors during reallocation?

std::vector provides the Strong Exception Guarantee: if resizing fails due to an exception, the vector must remain in its original state before the call. If a move constructor throws an exception halfway through moving elements to the new buffer, the moved elements in the new buffer cannot be safely moved back (their source state in the old buffer was already destroyed). To prevent irreversible state corruption, std::vector uses std::move_if_noexcept. If noexcept is missing, it falls back to copying elements.

01

Interviewer's lens

What separates a strong C++ candidate from a textbook answer

Copying is like building a duplicate house brick by brick: expensive, slow, and demands fresh land. Moving is simply handing over the house keys and changing the mailbox name: the house stays right where it is, taking a single instant.

Interviewers test move semantics to see whether you picture CPU registers and memory transfers or just repeat buzzwords. Weak candidates believe std::move generates runtime machine instructions or moves memory itself. Senior candidates articulate three core engineering truths:

  • Pointer Stealing vs Deep Copying: Moving copies an 8-byte address in CPU registers in O(1) and zeros out the source pointer, bypassing the OS memory allocator entirely.
  • The std::move Reality: It produces zero CPU instructions. It is purely a compile-time static cast to an rvalue reference (T&&) that tells overload resolution it is safe to plunder the source.
  • The noexcept Reallocation Contract: std::vector must uphold the Strong Exception Guarantee during resizing. If your move constructor lacks noexcept, std::vector defensively falls back to slow O(N) deep copies for every element.
02

Under the hood & memory layout

Hardware & memory representation

At the hardware level, copying vs moving is the difference between duplicating an entire estate and handing over the house keys:

  • Deep Copying: The OS allocator scans the heap for free space, reserves new memory pages, and runs memcpy over every byte (an expensive O(N) operation).
  • Moving (Pointer Stealing via Register Swap): The CPU copies the 8-byte pointer address from the source register to the destination register (mov rax, [rsi]; mov [rdi], rax) and writes 0x0 (nullptr) into the source. The heap buffer never moves in RAM; only the ownership key changes hands in an instantaneous O(1) register swap.

This resource transfer relies on three mechanical pillars:

  1. Zero-Instruction Cast: std::move(x) produces zero machine code instructions. It is merely a compile-time type cast static_cast<std::remove_reference_t<T>&&>(x) that unlocks move overloads.
  2. Source Nullification: The source pointer is wiped to nullptr. When the source's destructor runs at scope exit, delete[] nullptr executes safely as a harmless no-op.
  3. The noexcept Contract: When std::vector doubles its capacity, it cannot risk half-moved objects if an exception strikes. It checks std::is_nothrow_move_constructible_v<T> via std::move_if_noexcept. If noexcept is missing, it refuses to move and falls back to deep copies.
Lvalue vs Rvalue in Registers/RAM

An Lvalue has an identifiable memory address (in the stack frame or heap) and persists across statements. An Rvalue (such as an ephemeral return value or literal) has no persistent identity and is about to expire at the semicolon.

Rvalue Reference (T&&)

8 bytes at ABI level (identical to T* const). Denotes a binding to an expiring object whose internal resources may be pilfered without violating program correctness.

Moved-From State

The C++ standard requires moved-from objects to be left in a 'valid but unspecified' state. In practice for resource owners, setting data_ = nullptr; size_ = 0; ensures the source's destructor executes as a harmless no-op.

Vector Reallocation Detection

std::vector::reserve or push_back queries noexcept(T(std::declval<T&&>())). If false, std::move_if_noexcept yields const T&, triggering deep copy construction for every single existing element.

03

Core concepts & syntax traps

01

Value Categories: Lvalues, Prvalues, Xvalues, and Rvalue References

In modern C++, expressions are categorized by two questions: Does it have an identifiable identity (memory address)? and Can its resources be stolen?

CategoryHas Identity?Can Be Stolen?Example
LvalueYesNoNamed variable x, arr[0], *ptr
Prvalue (Pure Rvalue)NoYesLiteral 42, temporary std::string("hi")
Xvalue (eXpiring Value)YesYesResult of std::move(x)
Glvalue (Generalized Lvalue)Yes-Lvalue or Xvalue
Rvalue-YesPrvalue or Xvalue

An rvalue reference (T&&) is an 8-byte reference that binds specifically to temporaries (prvalues) and expiring objects (xvalues), giving explicit permission to plunder their resources.

C++23 snippet
int a = 10;            // 'a' is an lvalue (has persistent memory address &a)
// int&& bad = a;      // ERROR: cannot bind rvalue reference to lvalue!
int&& r1 = 42;         // Legal: binds to temporary prvalue
int&& r2 = std::move(a); // Legal: std::move casts lvalue 'a' to xvalue
Interview takeaway:

Lvalues have names and persist; rvalues are unnamed temporaries or explicitly moved values about to expire.

02

What std::move Really Does: The Zero-Cost Static Cast

The #1 myth in modern C++ is that std::move(x) moves data. std::move moves nothing and emits zero CPU instructions.

Mechanically, it is nothing more than a static type cast:

template <typename T>
constexpr std::remove_reference_t<T>&& move(T&& t) noexcept {
    return static_cast<std::remove_reference_t<T>&&>(t);
}
  1. A Sticky Label: It takes a named variable (lvalue) and relabels it as an expiring object (xvalue).
  2. Overload Signal: This label triggers the compiler to select move overloads (T(T&&)) rather than copy overloads (const T&).
  3. The Actual Steal: The move only occurs when that labeled reference is passed into a move constructor or move assignment operator that actually steals the pointer.
C++23 snippet
std::string s = "very long heap allocated string";
// std::move(s) alone does NOT clear 's'!
std::move(s); 
std::println("s is still: {}", s); // 's' is completely untouched!

// Overload resolution selects move constructor:
std::string s2 = std::move(s); 
// NOW 's' has been emptied because std::string's move ctor stole its buffer!
Interview takeaway:

std::move is just a compile-time cast to an rvalue reference; the actual move happens only when passed into a move-aware function.

03

Anatomy of the Move Constructor & Move Assignment Operator

Implementing a move operation is a two-step house key handover: steal the pointer, then zero out the old owner's hand.

  1. The Steal: Copy the raw pointer address and size members directly into this via fast register copy.
  2. The Nullification: Overwrite the source object's pointer with nullptr. If you skip this, the source destructor will destroy the very buffer this just acquired, causing a fatal double-free!
  3. The Assignment Guard: In operator=(T&&), always guard against self-move (if (this == &other) return *this;), free any existing buffer *this currently owns, steal the incoming pointer, and nullify the source.
C++23 snippet
class Buffer {
    int* data_{nullptr};
    size_t size_{0};
public:
    // Move Constructor: Steal and Nullify
    Buffer(Buffer&& other) noexcept : data_(other.data_), size_(other.size_) {
        other.data_ = nullptr; // Crucial: prevents double free!
        other.size_ = 0;
    }
    
    // Move Assignment: Free Old, Steal New, Nullify Source
    Buffer& operator=(Buffer&& other) noexcept {
        if (this == &other) return *this;
        delete[] data_;        // Free current buffer
        data_ = other.data_;   // Steal
        size_ = other.size_;
        other.data_ = nullptr; // Nullify
        other.size_ = 0;
        return *this;
    }
};
Interview takeaway:

Always nullify the source's pointers in move operations to prevent double-free crashes when the source is destroyed.

04

Why noexcept on Move Operations Is Mandatory (The Vector Growth Trap)

When std::vector grows its capacity, it must migrate elements to a newly allocated heap buffer while upholding the Strong Exception Safety Guarantee.

Why noexcept is mandatory in 3 steps:

  1. The Rollback Problem: If a copy constructor throws during migration, the vector discards the new memory; the old elements were untouched. But if an un-guaranteed move constructor throws halfway through, earlier elements are already hollowed out (nullptr)β€”state rollback is impossible without data loss!
  2. The Safety Gate: std::vector inspects std::is_nothrow_move_constructible_v<T> using std::move_if_noexcept.
  3. The Silent Penalty: If your move constructor lacks noexcept, std::vector refuses to move your objects, silently falling back to slow O(N) deep copies during every capacity expansion!
C++23 snippet
struct Slow {
    Slow(Slow&&) { /* not noexcept */ }
};
struct Fast {
    Fast(Fast&&) noexcept { /* noexcept guarantee */ }
};

static_assert(!std::is_nothrow_move_constructible_v<Slow>); // Vector copies Slow!
static_assert(std::is_nothrow_move_constructible_v<Fast>);  // Vector moves Fast!
Interview takeaway:

Always mark move constructors and move assignment operators noexcept so standard containers can safely use them.

05

Rule of 5 vs Rule of 0: When to Write Them and When to Avoid Them

Managing special member functions follows two modern doctrines:

  1. The Rule of 5: If you define any one of the five special functions (Destructor, Copy Ctor, Copy Assign, Move Ctor, Move Assign), you should declare all five. Declaring a custom destructor or copy operation suppresses automatic generation of move operations, silently causing std::move to fall back to expensive deep copies!
  2. The Rule of 0 (Preferred): Design classes that manage zero raw resources. Store data in standard RAII members (std::vector, std::string, std::unique_ptr).
  3. Zero Maintenance: With Rule of 0, the compiler automatically generates all five special member functions correctly with zero bugs and zero maintenance.
C++23 snippet
// RULE OF 0 (Preferred in modern C++):
class EmployeeRecord {
    std::string name_;
    std::vector<int> badgeNumbers_;
    std::unique_ptr<AuditLog> log_;
    // All 5 special member functions generated automatically and correctly!
};
Interview takeaway:

Prefer the Rule of 0 using RAII members; if managing raw handles, strictly adhere to the Rule of 5.

Brute2Best Pro Content

Unlock Live Implementation & Interview Scenarios

The C++23 production code, live coding steps, scenario twist solutions, and common UB traps are part of Brute2Best Pro. Module 1 is completely free.

All 440 visual dry runs & algorithms
Interactive step-by-step playback with pointer & array state transitions
52 comprehensive LLD chapters
Interactive UML diagrams, real-world stories (Food Delivery, Trading, RPG), and practice solutions
40 C++ basics labs & hardware memory models
Vtables, byte layouts, assembly insights, and multi-threading race conditions
Production-ready C++23 source code
Modern idioms, clean contracts, time/space complexity derivations, and interview takeaways

First anchor question per pattern, Part 1 LLD & Module 1 C++ basics are 100% Free forever.

Next In Your Mastery Journey

Apply C++ Internals to LLD & Algorithmic Patterns

Full-stack systems engineering