[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::movemistakes - 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
- What is RVO?
- What is NRVO?
- Copy elision guarantees in C++17
std::movemistakes- Performance measurement
- 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
| Topic | RVO | NRVO |
|---|---|---|
| What is returned | Temporary (prvalue) | Named local |
| C++17 mandatory elision | Yes (for the standard prvalue cases) | No (still optional) |
| Rule of thumb | return Type(args); | return name; on eligible paths |
Core rules
- Avoid
return std::move(local)for local objects you could return by name. - Prefer a single returned name on all paths when you rely on NRVO.
- Rely on C++17 rules for prvalue returns; understand NRVO remains optional.
- Implement move operations so that when elision does not fire, the fallback stays cheap.
Checklist
- No
std::moveon plain local returns? - Return paths structured for NRVO where it matters?
- No reference return from locals?
- Move (and copy) constructors sane if elision fails?
Related reading (internal)
Posts that connect naturally to this topic:
- C++ move semantics —
std::moveguide - C++ copy elision — overview
- C++ rvalue references and value categories
- C++ move constructor
Closing
RVO and related elisions are among the most important return-path optimizations in C++.
Principles:
- Do not slap
std::moveon every return. - Return one local name consistently when you want NRVO-friendly shapes.
- 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.