Copy elision & RVO/NRVO

RoadmapsC++

Scenario

You write a function that returns a massive 1-Gigabyte `Matrix` object by value. A colleague warns you that this will trigger an expensive copy constructor, halting the program for seconds. You run it, and it executes instantly.

Why didn't C++ copy the massive object when you returned it by value?

Why it exists

Historically in C and early C++, returning large structs by value was forbidden because it caused massive memory copies. Developers relied on passing output pointers (`void getResult(Matrix* out)`), which made APIs clunky. Copy Elision allows developers to write clean, mathematical syntax (`Matrix m = getResult();`) while enjoying zero-overhead, bare-metal performance.

Mental model

Imagine you are building a Lego castle on a small tray, and you plan to move it to a larger display table when finished. Copy elision is like building the castle directly on the display table from the start, skipping the risky, time-consuming move completely.

Copy Elision is a compiler optimization where unnecessary copying or moving of objects is omitted. Return Value Optimization (RVO) and Named Return Value Optimization (NRVO) allow a function to construct its return value directly in the memory space of the caller, completely bypassing the copy and move constructors.

Explanation

**1. Return Value Optimization (RVO):**

When you return a temporary, unnamed object from a function (`return Matrix();`), the compiler doesn't construct it locally and then copy/move it to the caller. Instead, the caller reserves memory, passes a hidden pointer to the function, and the function constructs the object directly in the caller's memory. In C++17, this is **mandatory**; it is guaranteed to happen, even if the copy constructor is deleted. (Note: Before C++17, because RVO was merely an optional optimization, the language strictly required a valid copy or move constructor to exist just in case the optimization failed).

**2. Named Return Value Optimization (NRVO):**

If you create a named local variable (`Matrix m; m.populate(); return m;`), the compiler attempts to do the same thing. Because the variable is named and manipulated before returning, this optimization is **optional** but performed by almost all modern compilers.

**3. Why `std::move` on return is an anti-pattern:**

A common mistake is writing `return std::move(m);`.

This forces the compiler to treat the return as an rvalue reference, triggering the move constructor. By doing this, you actively **disable NRVO**. While a move is cheap, NRVO is completely free (zero operations). Never use `std::move` on a returned local variable.

**4. Pass-by-Value and Copy Elision:**

If you pass a temporary object to a function taking an argument by value (`func(Matrix());`), the compiler elides the copy and constructs the temporary directly into the function's parameter memory.

Code examples

RVO, NRVO, and the Move Anti-Pattern

struct Heavy {
    Heavy() { cout << "Constructed\n"; }
    Heavy(const Heavy&) { cout << "Copied\n"; }
    Heavy(Heavy&&) { cout << "Moved\n"; }
};

// RVO: Mandatory in C++17. Constructs directly in caller's memory.
Heavy getHeavyRVO() {
    return Heavy();
}

// NRVO: Optional but standard. 'h' is built in the caller's memory.
Heavy getHeavyNRVO() {
    Heavy h;
    // ... do work on h ...
    return h;
}

// ANTI-PATTERN: std::move prevents NRVO!
Heavy getHeavyBad() {
    Heavy h;
    return std::move(h); // Forces "Moved" constructor. Slower than NRVO!
}

If you call `Heavy obj = getHeavyRVO();`, you will only see "Constructed" printed once. There is no copy and no move.

Key points

Common mistakes

Recall questions

Questions & answers

You write a class where you explicitly delete the copy and move constructors (`Heavy(const Heavy&) = delete;`). A function returns `Heavy()` by value. In C++11, this code fails to compile. In C++17, it compiles and runs perfectly. Why?

In C++11, copy elision was an optional optimization; the language still required a valid copy/move constructor to exist just in case. In C++17, RVO became mandatory (Guaranteed Copy Elision). Because the copy/move will strictly never happen, the compiler no longer requires those constructors to exist.

Approach: Understand the shift from 'optional optimization' to 'language guarantee' in modern C++.

A developer tries to rely on NRVO in a function with multiple return paths: `if(flag) return a; else return b;`. They notice the move constructor is suddenly being called instead of elided. Why?

NRVO requires the compiler to know exactly which local variable will become the return value so it can construct it in the caller's memory from the start. With multiple conditional returns returning different variables, the compiler cannot predict which one to construct in the return slot, forcing it to fall back to a move operation.

Approach: Identify the structural limits of compiler static analysis for optimizations.

Continue learning

Previous: Rule of 3 / 5 / 0 - copy, move, destructor

Return to C++ Roadmap