cpp-copy-elision

What Is Copy Elision and How It Works in C++

  • 4 min

The Copy Elision is an optimization that avoids creating intermediate copies by constructing the object directly in its final destination.

We’ve come a long way optimizing our C++ code.

  1. We learned that Copying is slow (it clones the entire object).
  2. We learned that Moving (std::move) is fast (it steals pointers).

It seems moving is the maximum speed we can aim for, right? Well, no. There’s something even faster than moving: Doing nothing.

Today we talk about Copy Elision.

It’s a technique (and since C++17, a guarantee) by which the compiler is so smart that it realizes it’s going to create a temporary object just to copy/move it somewhere else… and decides to skip the intermediate step.

The compiler constructs the object directly in its final destination.

The Experiment: A “Noisy” Object

To see this in action, we need a class that shouts at us every time we touch it. Let’s temporarily disable optimization mentally and see what should happen according to classical theory.

#include <iostream>

struct Noisy {
    Noisy() { std::cout << "Constructor\n"; }
    ~Noisy() { std::cout << "Destructor\n"; }
    
    // Copy Constructor
    Noisy(const Noisy&) { std::cout << "Copy\n"; }
    
    // Move Constructor
    Noisy(Noisy&&) noexcept { std::cout << "Move\n"; }
};

Noisy createObject() {
    return Noisy(); // Creates a temporary and returns it
}

int main() {
    std::cout << "--- Start ---\n";
    Noisy myObject = createObject();
    std::cout << "--- End ---\n";
}
Copied!

What We Would Expect Without Copy Elision

If we follow strict step-by-step logic:

  1. createObject calls the Constructor to create the temporary.
  2. The return Moves (or Copies) that temporary to myObject in main.
  3. The temporary inside the function is destroyed.
  4. At the end of main, myObject is destroyed.

We would expect to see: Constructor -> Move -> Destructor -> Destructor.

What Actually Happens With Copy Elision

If you compile that code (even without optimization flags on modern compilers), you’ll see this:

--- Start ---
Constructor
--- End ---
Destructor
Copied!

No copy! No move!

The compiler has seen that myObject is the final destination of the object created inside the function. So, instead of creating it inside the function and moving it, it passes the memory address of myObject to the function and constructs it directly there.

RVO and NRVO

This magic has technical names.

RVO (Return Value Optimization)

This occurs when we return an unnamed temporary (a prvalue).

return Noisy(); // RVO
Copied!

Since C++17, this is not an optional optimization. It is mandatory. It’s known as Guaranteed Copy Elision. This means that even if your class has the copy/move constructor deleted (delete), the code will still compile! Because technically, neither is being called.

NRVO (Named Return Value Optimization)

This occurs when we return a named local variable.

Noisy create() {
    Noisy local; // Named variable
    // ... do stuff with local ...
    return local;  // NRVO
}
Copied!

Here the compiler tries to align the local variable with the memory location where the calling function expects the result. This is still an optimization (not mandatory by the standard), but all decent compilers do it.

The std::move Anti-Pattern in return

A common mistake when learning std::move is trying to force a move when returning a local variable.

They think: “Since I’m going to return a local variable and I want it to be efficient, I’ll force the move.”

// ❌ WRONG: You break NRVO
Noisy badFunction() {
    Noisy local;
    return std::move(local); // ERROR!
}
Copied!

Why is this wrong? By doing std::move(local), you turn the expression into an X-value and it no longer meets the form required for NRVO. Typically, this causes a call to the move constructor.

By forcing the move, you prevent the compiler from performing Copy Elision.

  • With std::move: Constructor call + Move Constructor call.
  • Without std::move: Only constructor call (direct construction).

Practical tip: do not use std::move in a return statement when returning a local variable. The compiler already knows it’s going to be destroyed and will optimize it better than you can.

Side Effects

Copy Elision has a peculiarity: it changes the observable behavior of the program.

Normally, optimizations (like loop unrolling) make the program run faster but do exactly the same thing. Copy Elision eliminates calls to constructors and destructors.

If your copy constructor had a “side effect” (like incrementing a global counter, printing to the screen, or opening a connection), that effect will not occur.

That’s why, in C++, copy/move constructors should not have critical side effects on the program’s logic, because you can’t fully rely on how many times they will be called.