Copy elision and guaranteed elision in C++.

Since C++17 Some Copies Are Not Allowed to Happen

I spent years in high-frequency trading environments where every nanosecond was a line item on a P&L statement, and I can tell you that most tutorials treat copy elision and guaranteed elision like some sort of magical, optional gift from the compiler gods. They teach it as an “optimization” that might happen if the compiler feels generous, which is fundamentally wrong. If you approach your code with that mindset, you aren’t writing robust systems; you’re just gambling on implementation details. Relying on the compiler to “decide” to be fast is a great way to end up with a production outage when a minor version bump changes the optimization heuristic.

I’m not here to give you the sanitized, textbook version of these rules. I want to talk about how the language actually behaves when you’re staring at a stack trace at 2:00 AM. I’m going to strip away the fluff and explain the mechanical reality of how objects are constructed—or rather, how they aren’t. By the end of this, you’ll understand exactly when the compiler is legally required to skip those copies, so you can stop guessing and start writing code that is predictably efficient.

Table of Contents

When C17 Mandatory Copy Elision Rewrites the Rules

When C17 Mandatory Copy Elision Rewrites the Rules

Before C++17, RVO (Return Value Optimization) was a courtesy. The compiler looked at your code, saw a temporary being returned, and thought, “I can probably avoid a copy here if I feel like it.” It was an optimization, which meant it was technically optional. This created a massive headache for developers trying to reason about performance, as you were essentially praying the compiler would be smart enough to apply return value optimization to your hot paths.

Then C++17 changed the game by introducing mandatory copy elision. We moved away from the old, messy way of thinking about moving objects and toward a stricter model based on prvalue and xvalue mechanics. Now, when you return a temporary, the compiler doesn’t just “optimize” the copy away; the object is constructed directly in the target memory location. The copy or move constructor isn’t just skipped—it’s not even called. It’s as if the intermediate object never existed. This shift effectively codified the difference between C++ move semantics vs copy elision, turning what used to be a “maybe” into a fundamental rule of the language.

Decoding Prvalue and Xvalue Mechanics Under the Hood

Decoding Prvalue and Xvalue Mechanics Under the Hood

To understand why the compiler is allowed to skip a copy, you have to stop thinking about “objects” as physical things and start thinking about value categories. Most tutorials treat prvalues and xvalues as academic trivia, but they are the actual gears turning in the machine. A prvalue (pure rvalue) is essentially a temporary instruction to create an object; it doesn’t “exist” in a specific memory location until it is initialized. When you return a temporary from a function, you aren’t moving a finished product from point A to point B. Instead, you are handing the compiler a blueprint that says, “Construct this directly into the caller’s stack frame.”

This is where the distinction between C++ move semantics vs copy elision becomes vital. A move operation still requires a constructor to run—it’s just a cheaper one. But with mandatory elision, the constructor for the “moved” object is never even called because the object is born in its final destination. When you’re dealing with prvalue and xvalue mechanics, you aren’t just optimizing; you are fundamentally changing the lifecycle of the data. If you assume a move is happening when the compiler has actually performed elision, you’re likely misunderstanding the very foundation of how your code allocates memory.

Rules of Engagement: How Not to Get Burned

  • Stop writing `auto x = MyClass();` to “save” a move. Since C++17, the compiler is already doing the work for you via mandatory elision; your manual optimizations are just noise that makes the code harder to read.
  • Never rely on the address of a temporary. If you assume a `prvalue` is a stable object in memory just because elision occurred, you’re playing with fire. The rules guarantee the object exists, but they don’t guarantee its lifetime matches your mental model.
  • Watch your move constructors. Mandatory elision bypasses the move constructor entirely. If you’ve left a side effect or a critical state change inside your move constructor that you expect to happen during a return, you’re going to find that code path is suddenly dead.
  • Don’t use `std::move` on a return statement. It is one of the most common ways to accidentally kill guaranteed elision. By casting a local variable to an `xvalue`, you’re telling the compiler, “Don’t elide this; I actually want to move it,” which forces a move instead of the zero-cost construction you actually wanted.
  • Test with different optimization levels, but don’t trust them. While C++17 made elision mandatory for certain patterns, complex template metaprogramming can still lead to edge cases where the compiler’s ability to elide depends on how much it can see through your abstractions. If the performance is critical, verify the assembly.

The Bottom Line

Stop relying on “optimization” as a mental model. C++17 changed elision from a compiler’s choice to a language requirement; if you’re writing code that assumes a move constructor will always trigger, you’re building on sand.

The value category of an expression dictates whether an object is actually constructed or merely moved. If you don’t understand the distinction between a prvalue and an xvalue, you’ll spend your debugging hours chasing ghosts in your move semantics.

Elision isn’t just about saving clock cycles—it’s about the fundamental existence of objects. Knowing the rules of guaranteed elision is the difference between writing code that works by accident and writing code that is architecturally sound.

The Bottom Line

At the end of the day, elision isn’t just a “nice-to-have” optimization that the compiler throws at you when it feels generous. Since C++17, it is a fundamental part of the language’s semantics. You have to stop thinking in terms of “moving” or “copying” objects and start thinking in terms of identity and lifetime. If you treat every return statement as a physical relocation of bytes, you’re going to write code that is either unnecessarily complex or fundamentally broken when you try to take addresses of temporary objects. Understanding the distinction between a `prvalue` and an `xvalue` is the only way to truly grasp why certain code compiles while others trigger a cryptic error in your template metaprogramming.

Don’t let the complexity of the value categories intimidate you. The goal isn’t to memorize every edge case in the standard, but to develop an intuition for how the object model actually behaves when the compiler starts stripping away the overhead. When you stop fighting the language and start working with its rules, you’ll find that C++ becomes significantly more predictable. Stop writing code that relies on what you hope the compiler does, and start writing code that leverages what it is guaranteed to do. That is where the real performance lives.

About Ruaridh Kensington-Oyelaran

C++ rewards people who know what the compiler is allowed to do. I write about the rules that bite, the ones nobody mentions until you have already shipped the bug.

More From Author

Comparing containers and their memory profiles.

A Linked List Is Slower Than a Vector Almost Every Time