I spent six years in high-frequency trading environments where every microsecond was a line item on a balance sheet, and if there is one thing I learned, it is that tutorials lie to you. Most of them treat return value optimisation explained as some magical, black-box gift from the compiler that you can just take for granted. They tell you it “just works,” but they never mention the edge cases where your code suddenly decides to perform a massive, unnecessary copy because you tripped over a move constructor or a `std::move` in a place it didn’t belong. Relying on “magic” is how you end up with jittery latency that you can’t profile your way out of.
I’m not here to give you a theoretical lecture on the C++ standard’s wording. Instead, I’m going to show you how the compiler actually handles these objects in the real world. We’ll look at where the optimization succeeds, where it silently fails, and how to write code that doesn’t rely on pure luck to stay performant. By the end of this, you’ll understand the mechanics well enough to stop praying and start predicting.
Table of Contents
The Copy Elision Mechanism How the Compiler Cheats

Most people think of a function return as a hand-off: the function creates an object, and then it hands it over to the caller. In reality, that’s a performance nightmare. If we actually followed the literal semantics of the language every time, we’d spend half our CPU cycles just shuffling bytes from one stack frame to another. Instead, the copy elision mechanism allows the compiler to effectively cheat. It looks at the destination where the caller wants the object and says, “Why bother making a temporary? I’ll just construct the object directly in the caller’s memory space.”
It is vital to distinguish between the different ways this happens. You have RVO, which handles unnamed temporaries, and named return value optimization (NRVO), which deals with objects that actually have a name within the function scope. While they feel similar, the rules governing them are distinct. If you’re relying on the compiler to save you, you need to realize that while RVO is mandatory in modern C++, NRVO is still technically an optimization. If you write code that prevents the compiler from seeing that direct path, you’ll end up falling back to move semantics—or worse, a full-blown copy.
Nrvo vs Rvo Differences When the Magic Fails

Here is where most developers trip up. You need to understand the distinction between RVO and named return value optimization (NRVO), because the former is a guarantee, while the latter is a polite request. When you return an anonymous temporary, the compiler is legally obligated to elide the copy. It’s clean, it’s mandatory, and it’s predictable. But the moment you give that object a name—say, `auto result = MyClass(); return result;`—you move into the territory of NRVO.
NRVO is not a mandate in the standard; it is a compiler optimization technique that the implementation is allowed to perform, but isn’t forced to. If your object’s lifetime is too complex or the control flow paths are too convoluted for the optimizer to track, the “magic” simply stops working. Instead of a seamless construction in the caller’s stack frame, you’re suddenly back to square one: relying on C++ move semantics to salvage what’s left of your performance. If you aren’t careful with how you structure your return paths, you’ll find yourself shipping code that works perfectly in debug builds but hits a performance wall in production.
How to stop fighting the compiler
- Stop returning by pointer or reference just to “avoid a copy.” You’re usually just trading a free optimization for a lifetime management nightmare. Let the compiler handle the object construction.
- Keep your return paths simple. If you start branching into complex conditional logic where different branches return different local objects, you’ll break Named Return Value Optimization (NRVO) and fall back to a move or a copy.
- Avoid `std::move` on your local return variables. It feels intuitive, but it actually prevents NRVO because you’re explicitly changing the type of the expression from an lvalue to an rvalue. The compiler can’t elide a copy if you’ve already told it the object is a temporary.
- Understand that RVO is a language guarantee, but NRVO is just a “maybe.” If your code relies on the performance of NRVO, you’re writing code that is technically correct but practically fragile.
- Use `[[nodiscard]]` on functions where the return value is critical. If the compiler is doing the heavy lifting of eliding copies to make your code fast, the last thing you want is a caller ignoring the result and wasting the entire effort.
The Bottom Line
Don’t try to outsmart the compiler by manually implementing move semantics for return values; in most modern contexts, the compiler will simply elide the copy or move entirely, making your “optimizations” redundant noise.
Distinguish between RVO and NRVO in your head. RVO is a guaranteed win for temporary objects, but NRVO is a courtesy that depends on the compiler’s ability to track a single named object through your function’s logic.
If you find yourself writing code that prevents elision—like returning a reference to a local or using `std::move` on a return value—you aren’t being clever; you’re actively sabotaging the very optimizations that keep your latency low.
The Bottom Line
At the end of the day, RVO and NRVO aren’t just “nice-to-have” optimizations; they are fundamental to writing modern, efficient C++. If you treat every returned object as a heavy, expensive copy operation, you’re fighting the language instead of using it. You need to understand the distinction between mandatory elision and the probabilistic nature of named return value optimization. If you write code that relies on specific compiler behaviors to stay performant, or worse, if you write code that accidentally inhibits these optimizations by returning a local variable through a pointer or a reference, you are leaving latency on the table. Stop treating the compiler like a black box and start treating it like a partner in the implementation details.
C++ is a language that demands respect, specifically respect for its complexity. It doesn’t forgive ignorance, but it rewards precision with unparalleled control. When you stop viewing these optimizations as magic and start seeing them as the logical extension of the object model, you stop being a coder and start being a systems programmer. Don’t just write code that works; write code that understands the machine. The goal isn’t to write clever tricks, but to write code where the intended behavior and the actual machine instructions are one and the same. That is where the real engineering happens.