Lvalues, Rvalues, Move Semantics & The Rule of 5 / 0
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).
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).
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&.
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.
Interviewer's lens
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::moveReality: 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
noexceptReallocation Contract:std::vectormust uphold the Strong Exception Guarantee during resizing. If your move constructor lacksnoexcept,std::vectordefensively falls back to slow O(N) deep copies for every element.
Under the hood & memory layout
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
memcpyover 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 writes0x0(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:
- Zero-Instruction Cast:
std::move(x)produces zero machine code instructions. It is merely a compile-time type caststatic_cast<std::remove_reference_t<T>&&>(x)that unlocks move overloads. - Source Nullification: The source pointer is wiped to
nullptr. When the source's destructor runs at scope exit,delete[] nullptrexecutes safely as a harmless no-op. - The
noexceptContract: Whenstd::vectordoubles its capacity, it cannot risk half-moved objects if an exception strikes. It checksstd::is_nothrow_move_constructible_v<T>viastd::move_if_noexcept. Ifnoexceptis missing, it refuses to move and falls back to deep copies.
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.
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.
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.
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.
Core concepts & syntax traps
Value Categories: Lvalues, Prvalues, Xvalues, and Rvalue References
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?
| Category | Has Identity? | Can Be Stolen? | Example |
|---|---|---|---|
| Lvalue | Yes | No | Named variable x, arr[0], *ptr |
| Prvalue (Pure Rvalue) | No | Yes | Literal 42, temporary std::string("hi") |
| Xvalue (eXpiring Value) | Yes | Yes | Result of std::move(x) |
| Glvalue (Generalized Lvalue) | Yes | - | Lvalue or Xvalue |
| Rvalue | - | Yes | Prvalue 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.
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 xvalueLvalues have names and persist; rvalues are unnamed temporaries or explicitly moved values about to expire.
What std::move Really Does: The Zero-Cost Static Cast
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);
}
- A Sticky Label: It takes a named variable (lvalue) and relabels it as an expiring object (xvalue).
- Overload Signal: This label triggers the compiler to select move overloads (
T(T&&)) rather than copy overloads (const T&). - 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.
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!std::move is just a compile-time cast to an rvalue reference; the actual move happens only when passed into a move-aware function.
Anatomy of the Move Constructor & Move Assignment Operator
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.
- The Steal: Copy the raw pointer address and size members directly into
thisvia fast register copy. - The Nullification: Overwrite the source object's pointer with
nullptr. If you skip this, the source destructor will destroy the very bufferthisjust acquired, causing a fatal double-free! - The Assignment Guard: In
operator=(T&&), always guard against self-move (if (this == &other) return *this;), free any existing buffer*thiscurrently owns, steal the incoming pointer, and nullify the source.
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;
}
};Always nullify the source's pointers in move operations to prevent double-free crashes when the source is destroyed.
Why noexcept on Move Operations Is Mandatory (The Vector Growth Trap)
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:
- 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! - The Safety Gate:
std::vectorinspectsstd::is_nothrow_move_constructible_v<T>usingstd::move_if_noexcept. - The Silent Penalty: If your move constructor lacks
noexcept,std::vectorrefuses to move your objects, silently falling back to slow O(N) deep copies during every capacity expansion!
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!Always mark move constructors and move assignment operators noexcept so standard containers can safely use them.
Rule of 5 vs Rule of 0: When to Write Them and When to Avoid Them
Rule of 5 vs Rule of 0: When to Write Them and When to Avoid Them
Managing special member functions follows two modern doctrines:
- 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::moveto fall back to expensive deep copies! - 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). - Zero Maintenance: With Rule of 0, the compiler automatically generates all five special member functions correctly with zero bugs and zero maintenance.
// 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!
};Prefer the Rule of 0 using RAII members; if managing raw handles, strictly adhere to the Rule of 5.
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.
First anchor question per pattern, Part 1 LLD & Module 1 C++ basics are 100% Free forever.