Skip to Content
Course content

124: Copy Elision and Return Value Optimization

Click on the "Edit" button in the top corner of the screen to edit your slide content.

A few years ago, I was reviewing code for a junior dev named Sarah who was working on a high-frequency trading module. She had written a function that generated a massive PriceHistory object—essentially a wrapper around a large std::vector. In an effort to be "efficient," she had wrapped the return statement in std::move(), thinking she was helping the compiler avoid a costly copy. When we ran the profiler, we noticed something strange: the code was actually slower, and the move constructor was being called when it didn't need to be. Sarah had accidentally blocked the compiler from performing an optimization that makes the cost of returning that object exactly zero.

The Magic of Return Value Optimization

In the early days of C++, returning a large object by value felt like a crime. You'd worry about the object being constructed locally, copied into a temporary, and then potentially copied again into the caller's variable. To avoid this, we used to pass objects by reference to be filled in, which made the code clunky and hard to read. Enter Return Value Optimization (RVO).

RVO is the compiler's ability to realize that the local object being returned and the object receiving the result in the calling function are, for all intents and purposes, the same thing. Instead of creating a local object and then copying it, the compiler simply constructs the object directly in the memory space reserved for the return value. No copy, no move, no overhead.

Then there is NRVO (Named Return Value Optimization). This is slightly more complex because it happens when you give the object a name before returning it. For example, if you calculate a result, store it in a variable res, and then return res;, the compiler can still often elide the copy. However, NRVO is an optional optimization—the compiler is allowed to do it, but it isn't required to. This is where Sarah's mistake happened. By adding std::move(res), she explicitly told the compiler to treat the return as an xvalue, which effectively disabled NRVO and forced a move operation where there could have been nothing at all.

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

BigData createData() {
    BigData data; 
    return data; // NRVO likely kicks in here
}

int main() {
    BigData myData = createData(); // Ideally: "Constructed" is printed once.
}

Mandatory Copy Elision in C++17

For a long time, copy elision was a "best effort" by the compiler. If you were writing a library that absolutely required no copies, you were playing a dangerous game of hoping the compiler was smart enough. C++17 changed the rules by introducing Mandatory Copy Elision.

Specifically, when you return a prvalue (a temporary object), the language specification now guarantees that no copy or move will occur. It's not that the compiler "optimizes" the copy away; it's that the copy simply doesn't exist in the eyes of the language. This is a huge win for us. It means you can write clean, functional-style code—returning objects by value—without any guilt about performance.

I'll give you a rule of thumb: just return your object by value. Don't use std::move on a local variable you're returning, and don't use pointers just to avoid a copy. Trust the compiler to do its job. If you're truly paranoid, you can always add log statements to your move and copy constructors during debugging to see what's actually happening under the hood.




📋 Practical Task

Detecting RVO and Move Pessimization in a Matrix Class

Your goal is to observe how the compiler handles object returns and identify when a "manual optimization" actually hurts performance.

The Setup: Create a class called Matrix that allocates a large array on the heap in its constructor. To track what the compiler is doing, implement the following:

  • A default constructor that prints "Constructed".
  • A copy constructor that prints "Copied".
  • A move constructor that prints "Moved".
  • A destructor that cleans up the memory.

The Experiment: 1. Write a function createMatrix() that creates a local Matrix object and returns it by value. Call this in main() and observe the output. 2. Modify createMatrix() to return the object using return std::move(matrix);. Observe how the output changes. 3. Change createMatrix() to return a temporary object directly (e.g., return Matrix();) and observe if the output differs from the first experiment.

Requirement: Ensure you compile with optimizations enabled (e.g., -O2) to see how a production compiler behaves, but also try it with optimizations disabled (-O0) to see the difference between mandatory elision and optional NRVO.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.