Copy Elision (RVO / NRVO) & Object Lifetime Traps
In modern C++, we never guess what the compiler does. We look directly at byte offsets, CPU registers, and object ownership lifetimes.
RVO (Return Value Optimization) applies when returning an unnamed prvalue (e.g. return Widget();). Since C++17, this is mandatory guaranteed copy elision: the language specification dictates that no temporary object is ever created; the object is constructed directly in the caller's storage. NRVO (Named Return Value Optimization) applies when returning a named local variable (e.g. Widget w; return w;). It is an optional compiler optimization where the compiler unifies the local variable's stack offset with the caller's return slot.
Because returning a named local variable allows the compiler to perform NRVO (0 copies, 0 moves). Writing std::move(local) casts the variable to an rvalue reference (T&&). The C++ standard explicitly prohibits NRVO when returning an rvalue reference. The compiler is then forced to construct the object locally, call the move constructor to transfer it into the caller's slot, and call the local destructor. Furthermore, if NRVO is impossible, the compiler automatically moves local variables anyway.
Objects that are too large for CPU registers (or types with non-trivial copy/move/destructor operations) use the hidden return pointer calling convention. The caller allocates space on its own stack frame and passes a hidden pointer to this uninitialized memory in the first argument register (rdi on Linux/macOS, rcx on Windows). The callee writes directly into this memory, avoiding any copy back to the caller.
Interviewer's lens
Instead of cooking a meal in the kitchen, packing it in a box, driving it across town, and unpacking it on the dining table, the chef simply walks into the dining room and cooks directly on the table. That is copy elision.
Interviewers use copy elision to separate developers who write code by superstition from engineers who understand the CPU calling convention and return slots. Weak candidates write return std::move(local); thinking they are optimizing code, completely blind to the fact that they just broke compiler NRVO. Senior engineers stand out by mastering three mechanical realities:
- The ABI Hidden Return Slot: Callers reserve the memory slot upfront and pass its address in register
rdi/rcx; the function constructs the object directly inside that caller slot. - C++17 Guaranteed Elision: Prvalues are not objects—they are initializers. Zero copies and zero moves are guaranteed by the standard, allowing return of types with deleted copy/move constructors.
- NRVO vs Pessimizing Move: Returning a named variable allows the compiler to overlay its storage onto the return slot. Wrapping it in
std::movedisables NRVO and forces an extra move construction and destructor call.
Under the hood & memory layout
Under standard 64-bit calling conventions (System V AMD64 and Windows x64), non-trivial objects cannot be returned in CPU registers. Instead, the caller reserves memory on its own stack frame and passes a hidden pointer to this uninitialized memory in register rdi (Linux) or rcx (Windows). The callee writes directly into this caller slot.
Three compiler mechanisms govern this process:
- C++17 Guaranteed Copy Elision (Prvalues): Returning a prvalue (
return Widget(42);) is not an optimization—it is a core language invariant. The prvalue acts as an in-place recipe, constructing the object directly in[rdi]with zero copies and zero moves. - Named Return Value Optimization (NRVO): When returning a named local variable (
Widget w; return w;), the compiler assignsw's local stack address directly to[rdi], eliminating all copies and moves across a unified return path. - The
return std::moveAnti-Pattern: Writingreturn std::move(w);casts the variable to an xvalue (Widget&&). The C++ standard explicitly forbids NRVO on rvalue references! The compiler is forced to allocate separate stack space forw, call the move constructor to populate[rdi], and runw's destructor on return.
The caller stack frame reserves storage for the return object and passes its address (0x7ffd20) secretly in register rdi / rcx. Callee writes directly to [rdi].
Prvalues have no storage of their own. return Widget(42); initializes the memory at [rdi] directly. No intermediate object is created anywhere on the stack.
When returning Widget w; return w;, the compiler's backend assigns the local variable w to memory address 0x7ffd20 (the caller's return slot). Zero moves or copies occur.
Binding a temporary directly to const T& or T&& extends the temporary's lifetime to the reference's scope. Crucially, lifetime extension is not transitive; chaining a member function (make().getName()) destroys the parent temporary at the semicolon.
Core concepts & syntax traps
Mandatory Copy Elision in C++17 (Guaranteed RVO)
Mandatory Copy Elision in C++17 (Guaranteed RVO)
Since C++17, copy elision for prvalues is no longer an optional optimization—it is a language guarantee.
- Prvalues Are Initializers: Expressions like
Widget(1, 2)ormakeWidget()do not create a temporary object that gets copied or moved; they are instructions on how to initialize the caller's target slot. - Zero Moves or Copies: The object is constructed directly in-place inside the destination memory address.
- Returning Immovable Types: Because no copy or move constructor is ever invoked, you can safely return types whose copy and move constructors are both explicitly
= deleted!
struct Immovable {
int data;
explicit Immovable(int d) : data(d) {}
Immovable(const Immovable&) = delete; // No copy
Immovable(Immovable&&) = delete; // No move
};
Immovable makeImmovable() {
return Immovable(42); // Guaranteed elision in C++17: compiles cleanly!
}
Immovable obj = makeImmovable(); // Constructed directly in 'obj' slot!C++17 prvalue return values construct directly in the caller's target slot, even for non-movable non-copyable types.
Named Return Value Optimization (NRVO)
Named Return Value Optimization (NRVO)
When a function returns a named local variable (Widget w; ... return w;), the compiler performs Named Return Value Optimization (NRVO).
- Address Aliasing: Instead of giving
wits own local stack frame slot and copying it out, the compiler assignsw's memory address to match the caller's return slot. - Zero Overhead: Construction happens directly inside the caller's memory from the start, achieving zero copies and zero moves at
-O2. - Unified Return Path: NRVO succeeds best when all return statements return the exact same local variable.
Widget createWidget() {
Widget local(100); // Constructed directly in caller's return slot
local.modify();
return local; // NRVO: 0 copies, 0 moves!
}Return named local variables directly; modern optimizing compilers construct them directly in the caller's destination.
The return std::move Anti-Pattern (Pessimizing Copy Elision)
The return std::move Anti-Pattern (Pessimizing Copy Elision)
One of the most widespread performance bugs in C++ is writing return std::move(local); under the false belief that it speeds up returns.
Why it actively ruins performance in 4 steps:
- Type Degradation: It changes the return expression from a named variable (
Widget) to an rvalue reference (Widget&&). - NRVO Disabled: The C++ standard explicitly prohibits NRVO when returning an rvalue reference.
- Forced Move & Destructor: The compiler is forced to allocate local stack space for
local, execute a move constructor into the caller's slot, and then executelocal's destructor. - The Clean Fix: Simply write
return local;. NRVO eliminates both the move and the destructor entirely!
// PESSIMIZING ANTI-PATTERN (DO NOT DO THIS!):
Widget bad() {
Widget w;
return std::move(w); // FORCES a move and destructor call; disables NRVO!
}
// CLEAN & OPTIMAL:
Widget good() {
Widget w;
return w; // NRVO constructs directly in caller: 0 moves, 0 extra dtors!
}Never write return std::move(local); it disables NRVO and pessimizes performance.
When NRVO Cannot Happen & Implicit Move Fallback
When NRVO Cannot Happen & Implicit Move Fallback
There are two situations where the compiler cannot perform NRVO:
- Multiple Distinct Return Variables:
if (flag) return a; else return b;. The compiler cannot alias two distinct objects onto a single return slot simultaneously. - Returning a Parameter:
Widget fn(Widget w) { return w; }. The parameter's memory layout is governed by the caller's argument passing rules.
The Automatic Safety Net: Even when NRVO is impossible, C++ automatically performs an implicit move. If NRVO cannot be applied, the compiler automatically treats return local; as an rvalue move without requiring std::move!
Widget select(bool cond) {
Widget a(1), b(2);
if (cond) return a; // Cannot NRVO both 'a' and 'b' to the same slot!
return b; // Compiler falls back to IMPLICIT MOVE (no std::move needed)
}Even when NRVO fails, C++ automatically moves local return variables; manual std::move is still unnecessary.
Temporary Lifetime Extension Rules & The Dangling Member Trap
Temporary Lifetime Extension Rules & The Dangling Member Trap
Binding a temporary object directly to a const T& or T&& extends the temporary's lifetime to match the lifetime of the reference.
However, lifetime extension is not transitive:
- Direct Binding (Safe):
const Data& ref = makeData();extends the temporary untilrefleaves scope. - Member Chaining (Deadly Trap): Writing
const std::string& name = makePerson().getName();destroys the temporarymakePerson()object at the semicolon! The returned referencenameis left dangling into dead memory, causing crashes or silent memory corruption. - Modern Fix: Use modern C++ ref-qualifiers (
const &for lvalues, and&&returning by value for rvalues) to prevent chaining references from expiring temporaries.
// SAFE: Directly bound temporary is extended
const Data& safeRef = makeData(); // Temporary lives until safeRef goes out of scope!
// DEADLY TRAP: Lifetime extension does not apply to member function returns!
const std::string& badRef = makePerson().getName();
// makePerson() is DESTROYED AT THE SEMICOLON! badRef is now dangling!Lifetime extension only applies to the directly bound temporary, never to references returned from its member functions.
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.