C++ RVO and NRVO: Copy Elision, Performance, and Why return
이 글의 핵심
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.
More related posts
- Browse the C++ series
- C++ Adapter pattern — interfaces and compatibility
- C++ ADL (argument-dependent lookup)
- C++ aggregate initialization
Frequently Asked Questions (FAQ)
Q. When would I use this in practice?
A. RVO vs NRVO, C++17 guaranteed elision for prvalues, why std::move on return blocks NRVO, benchmarks, and a practical.
Q. What should I read before this?
A. Follow the previous article or related articles links at the bottom of each post to learn in sequence. See the C++ series index for the full picture.
Q. Where can I study this more deeply?
A. Check cppreference and the relevant library’s official documentation. The reference links at the end of the article are also worth using.
Related Articles (Internal Links)
Other articles related to this topic.
- C++ RVO and NRVO | Return Value Optimization Complete Guide
- C++ Copy Elision | When Copies and Moves Disappear
- C++ Move Errors
Keywords Covered in This Article (Related Search Terms)
This article covers C++, RVO, NRVO, copy elision, optimization, C++17, performance.