[2026] C++ RVO and NRVO: Copy Elision, Performance, and Why `return std::move` Hurts

[2026] C++ RVO and NRVO: Copy Elision, Performance, and Why `return std::move` Hurts

이 글의 핵심

A practical guide to C++ RVO and NRVO: when the compiler omits copies on return, what C++17 guarantees, how NRVO differs, why `return std::move(local)` is usually wrong, and how to measure the difference.

Introduction: “Should I use std::move on return?”

“Copies are not happening when I return from a function”

In C++, copy elision on return is often explained in terms of RVO (Return Value Optimization).

// ❌ std::move is unnecessary and often harmful here
std::string foo() {
    std::string result = "Hello";
    return std::move(result);  // ❌ blocks NRVO
}

// ✅ Prefer a plain return
std::string foo() {
    std::string result = "Hello";
    return result;  // ✅ copy/move may be elided (NRVO)
}

This article covers:

  • RVO and NRVO
  • Mandatory copy elision in C++17 (for prvalues)
  • Common std::move mistakes
  • Benchmarking

What it looks like in real projects

When you learn from books, examples are tidy and theoretical. Production code is not: legacy constraints, deadlines, and bugs you did not anticipate. The ideas here are ones I first met as theory, then internalized after seeing them break in real code reviews.

What stuck with me was an early project where I followed a recipe from a book and could not understand why behavior diverged for days. A senior developer caught it in review. This post pairs the rules with pitfalls you are likely to hit in practice.

Table of contents

  1. What is RVO?
  2. What is NRVO?
  3. Copy elision guarantees in C++17
  4. std::move mistakes
  5. Performance measurement
  6. Summary

1. What is RVO?

RVO (Return Value Optimization)

RVO is an optimization that elides copy/move operations when values are returned from functions.

struct Data {
    std::vector<int> vec;

    Data() {
        std::cout << "Constructor\n";
    }

    Data(const Data&) {
        std::cout << "Copy Constructor\n";
    }

    Data(Data&&) noexcept {
        std::cout << "Move Constructor\n";
    }
};

// RVO example
Data createData() {
    return Data();  // return a temporary
}

int main() {
    Data d = createData();
    // Typical output: Constructor only (no copy/move in the return path)
}

RVO in practice:

  • Applies when returning a temporary (a prvalue) of the function’s return type.
  • Under C++17, mandatory copy elision applies in the standard cases for such returns.

2. What is NRVO?

NRVO (Named Return Value Optimization)

NRVO elides copy/move operations when you return a named local variable.

// NRVO example
Data createData() {
    Data result;  // named local
    result.vec.push_back(42);
    return result;  // NRVO may apply
}

int main() {
    Data d = createData();
    // Often: Constructor only (no copy/move on return)
}

NRVO expectations:

  • You return a named local object.
  • All return paths that participate in NRVO return the same variable (simplified rule of thumb).
  • Even in C++17, NRVO is not guaranteed; it remains a quality-of-implementation optimization where permitted.

3. Copy elision guarantees in C++17

Before C++17: optional

// C++14
Data createData() {
    return Data();  // RVO possible (not guaranteed)
}

// If the compiler does not elide:
// 1. Move constructor
// 2. If no move, copy constructor

C++17 onward: prvalue returns are covered by the rules

// C++17
Data createData() {
    return Data();  // mandatory elision in the standard cases for this pattern
}

// Move/copy constructors may be unnecessary for this return path
struct NonMovable {
    NonMovable() = default;
    NonMovable(const NonMovable&) = delete;
    NonMovable(NonMovable&&) = delete;
};

NonMovable createNonMovable() {
    return NonMovable();  // ✅ well-formed in C++17 with mandatory elision
}

4. std::move mistakes

Mistake 1: std::move on return

// ❌ Typically prevents NRVO
std::string foo() {
    std::string result = "Hello";
    return std::move(result);  // expression is an xvalue, not a named return in the NRVO sense
}

// ✅ Prefer this
std::string foo() {
    std::string result = "Hello";
    return result;  // NRVO may apply
}

Why: std::move produces an rvalue reference to the object. You are no longer returning the name result in the way NRVO is specified to recognize; you are returning a different kind of expression, so optimizers usually treat this path less favorably than a plain return result;.

Mistake 2: Multiple return names

// ❌ NRVO generally does not apply across different names
std::string foo(bool flag) {
    std::string a = "A";
    std::string b = "B";

    if (flag) {
        return a;
    } else {
        return b;  // different variable
    }
}

// NRVO-friendly shape: one name returned on all paths (when possible), or accept move fallback

Mistake 3: Returning a reference to a local

// ❌ Undefined behavior: dangling reference
std::string& foo() {
    std::string result = "Hello";
    return result;
}

// ✅ Return by value
std::string foo() {
    std::string result = "Hello";
    return result;  // NRVO may apply
}

5. Performance measurement

Benchmark sketch (Google Benchmark)

#include <benchmark/benchmark.h>

struct Data {
    std::vector<int> vec;
    Data() : vec(1000000, 42) {}
};

// Elision-friendly return (prvalue)
static void BM_RVO(benchmark::State& state) {
    for (auto _ : state) {
        Data d = [] { return Data(); }();
        benchmark::DoNotOptimize(d);
    }
}
BENCHMARK(BM_RVO);

// std::move on a named local (typically blocks NRVO)
static void BM_Move(benchmark::State& state) {
    for (auto _ : state) {
        Data d = [] {
            Data result;
            return std::move(result);
        }();
        benchmark::DoNotOptimize(d);
    }
}
BENCHMARK(BM_Move);

Example results (GCC 13, -O3, illustrative):

BM_RVO     1000000 ns
BM_Move    1500000 ns  (~50% slower in this scenario)

Exact numbers depend on type, size, compiler, and flags; treat this as motivation to measure, not as a universal constant.


Summary

RVO vs NRVO

TopicRVONRVO
What is returnedTemporary (prvalue)Named local
C++17 mandatory elisionYes (for the standard prvalue cases)No (still optional)
Rule of thumbreturn Type(args);return name; on eligible paths

Core rules

  1. Avoid return std::move(local) for local objects you could return by name.
  2. Prefer a single returned name on all paths when you rely on NRVO.
  3. Rely on C++17 rules for prvalue returns; understand NRVO remains optional.
  4. Implement move operations so that when elision does not fire, the fallback stays cheap.

Checklist

  • No std::move on plain local returns?
  • Return paths structured for NRVO where it matters?
  • No reference return from locals?
  • Move (and copy) constructors sane if elision fails?

Posts that connect naturally to this topic:


Closing

RVO and related elisions are among the most important return-path optimizations in C++.

Principles:

  1. Do not slap std::move on every return.
  2. Return one local name consistently when you want NRVO-friendly shapes.
  3. C++17 tightens the language rules for prvalue returns; NRVO is still best-effort.

return std::move(local) on a local you could name usually hurts the optimization story compared with return local;.

Next step: go deeper with the C++ move semantics guide.