C++ RVO and NRVO | Return Value Optimization Complete Guide
이 글의 핵심
RVO vs NRVO: when the compiler elides copies on return, C++17 guaranteed elision for prvalues, NRVO heuristics, and interaction with move semantics.
1. Introduction: copy elision and optimization families
Copy elision is a compiler transformation that omits the copy or move operation that the C++ abstract machine would otherwise perform, by constructing the object directly in the storage of the target (e.g. the caller’s return slot or the variable being initialized). The program’s observable results stay the same (aside from side effects in copy/move constructors, which is why this optimization is not always allowed in older standards).
In practice, C++ return-path optimizations fall into a few overlapping categories:
- RVO (Return Value Optimization) — returning a temporary (typically a prvalue) of the return type, so the object can be created in the final destination.
- NRVO (Named Return Value Optimization) — returning a single named local with the same type as the return type, so the compiler can reuse the return slot and avoid a copy or move.
- Guaranteed copy elision (C++17+) — for specific prvalue flows, the standard requires that no
copy/moveobject is made in certain initialization contexts, independent of the compiler’s “optimization” heuristics. - Parameter elision (constructor / by-value call) — constructing directly into a parameter or subobject, often discussed alongside return optimizations.
Clarity: RVO and NRVO are not separate language features in the standard text the way one keyword is; they are compiler patterns and standard phrasing (especially [class.temporary] and copy elision in [class.copy.elision] in the Standard) for “construct in place instead of copy/move”.
I thought move was free until I actually watched the stack
I used to treat std::move as “basically free” for std::vector and std::string—and next to a deep copy, it often is. Then I was chasing a hot path in a return-heavy API and instrumented a type that logged every copy and move, sometimes with -fno-elide-constructors to force the abstract machine to show its work. The gap between return v; and return std::move(v); was not academic: extra pairing of constructors and destructors, extra pointer shuffles, and a real bump in the profile when the compiler would otherwise have elided the whole return path. I had been “helping” the optimizer into a worse place. That is the thread that runs through the rest of this: elision when you can get it, move as the honest fallback, and do not equate “move” with “no work.”
2. RVO: returning a temporary (prvalue)
RVO in everyday speech is the case:
std::string make_label(int id) {
return std::string("id=") + std::to_string(id);
}
Here the returned expression is a prvalue of type std::string (or an intermediate of the right type, depending on overload resolution). A quality implementation builds the result directly in the return value slot (often via hidden pointer / ABI conventions), without first constructing an anonymous local in the callee stack frame and then moving it.
A minimal shape that is easy to reason about is:
Widget factory() {
return Widget(42);
}
Why it matters — for large or expensive types, avoiding an extra move (or a copy when the type is not movable) is the difference between “free” and “O(n) work every call.”
C++17 and beyond — for many return / initialization chains involving prvalues, elision is no longer “only an optimization” but part of the mandatory initialization semantics. See Section 4.
3. NRVO: returning a named local
NRVO is the named case:
std::vector<int> make_sequence(int n) {
std::vector<int> v;
v.reserve(n);
for (int i = 0; i < n; ++i) { v.push_back(i); }
return v; // NRVO: single named return object
}
Unlike prvalue elision, NRVO is not fully standardized as mandatory in the same way across all C++ versions for all return paths. A compiler applies data-flow and ABI-aware heuristics; when NRVO does not apply, the fallback is usually a move (C++11+), and if the type is not movable, a copy if allowed.
A mental model that helps debugging: one clear object of return type, one return of that object (modulo some compiler-specific merging), no tricks that make the return expression a different kind of value (e.g. std::move on the name).
4. Guaranteed copy elision: C++17 mandatory cases
C++17 changes the meaning of certain initializations: some operations that look like they require a copy constructor do not in the language semantics anymore—there is no temporary to copy from.
A canonical example is returning a prvalue and binding it to a name without a separate temporary object in the standard’s higher-level model:
Widget make_widget() {
return Widget(1, 2, 3);
}
Widget w = make_widget();
Guaranteed elision (informal term for the combination of the new prvalue materialization rules) is why the following is valid even when copy and move are deleted:
struct OnlyConstruct {
OnlyConstruct() = default;
OnlyConstruct(OnlyConstruct const&) = delete;
OnlyConstruct(OnlyConstruct&&) = delete;
};
OnlyConstruct f() {
return OnlyConstruct{}; // ok: prvalue in return + elided chain
}
Important — the “guaranteed” story applies to certain prvalue paths; NRVO remains a best-effort optimization in many situations (even if in practice, mainstream compilers are excellent).
5. Move semantics: how move relates to elision
C++11 introduced rvalue references and “move” operations; copy elision is still preferred to moving when the compiler is allowed to construct the object in its final home.
Priority (simplified) when returning a local x of type T by value:
- Try NRVO (construct
xin the return slot or equivalent). - If elision is not possible, treat the operand as an rvalue if it is a function return of a local automatic object eligible for implicit move (see [class.copy.elision] in the current Standard).
- If move is not available or not viable, use copy if possible.
Not a priority rule in the spec the way “NRVO if possible” is written in a single line—but the outcome for modern compilers matches this on typical code.
Example where move is the practical fallback (NRVO not applicable):
std::vector<int> either(bool pick) {
std::vector<int> a{1, 2, 3};
std::vector<int> b{4, 5, 6};
return pick ? a : b; // two distinct objects: NRVO often fails
}
6. Compiler behavior: GCC, Clang, MSVC
What is portable is the C++ standard guarantee for the guaranteed prvalue cases. NRVO quality varies in edge cases, but the three main compilers (GCC, Clang, MSVC) all implement strong NRVO for the common pattern “single named variable of return type returned by name.”
I do not bet shipping code on a spreadsheet of which compiler is “1% better” on NRVO edge cases. In practice, if you hand Godbolt a plain return T{args} or a single T v; and return v;, GCC and Clang both look excellent on prvalue return and the boring named-local case; I still sanity-check MSVC for anything where /std and version matter for C++17 parity, because that is where I have seen surprises at the margin, not in “does NRVO exist.” Across all three, return std::move(local); is the same story: it often blocks NRVO and forces a move when a plain return local; would have been cheaper. Inlining and LTO can blur the picture across translation units, but they do not change the rule I follow: idiomatic by-value return first, std::move on the return of a local only when I have already given up on elision and measured that I need the rvalue.
Actionable advice — do not micro-optimize for one compiler. Write standards-idiomatic code: return by value for owning objects, avoid return std::move(local);, and read the assembly in hot paths.
7. When elision happens (conditions and patterns)
Strong patterns (RVO / guaranteed prvalue):
return T(args...);orreturn (expr)where the result is a prvalue of typeT.- Chained factory calls where each step returns a prvalue (multiple layers may elide in one initialization).
Strong patterns (NRVO — typical compiler success):
- A single automatic variable
T result;filled in some sequence, andreturn result;. - A single
T v = ...;and only onereturnthat returnsvin all exiting paths, or compatiblereturnforms as accepted by the compiler (simple functions).
Helping the compiler (without trying to be clever):
- Keep one logical “output” object in the function if you can.
- Avoid returning different named variables (see Section 8).
- Avoid
std::moveon a local you want NRVO to fire on.
8. When elision doesn’t happen: blocking factors
Common blockers (examples beat a grid here).
return std::move(x); for a local x — This is the one I still see in code review. The name is already treated as a move candidate in many return paths; adding std::move changes the value category in a way that can preempt NRVO and force a move. I fix it to plain return x; unless I have a measured reason not to.
return a ? b : c; with two different named locals — The compiler may not be able to treat that as a single “named return object” in the IR, so I either return prvalues per branch (return std::vector<int>{1}; / return std::vector<int>{2};), or build one T out and fill it, then return out; when the flow allows.
Return paths that go through odd conversions — If the type of the return expression wobbles, you can get extra temporaries. I make the return type explicit at the function level and return a clear T with an explicit conversion inside the function body.
Returning a member of *this — That is not the same as NRVO of a local automatic; the lifetime story is different. I might still return std::move(member); in specific methods when I am done with the object, but I do not confuse that with return w; from a stack local.
Returning a by-value parameter — The ABI and “is this a local in the same sense?” story is muddier than a single named stack variable. I try not to rely on NRVO of parameters portably; if the hot path is weird, I restructure or accept copy/move and measure.
A frequently cited blocker is returning a const value; const affects whether implicit move is possible. I keep return types non-const for movable values when I care about the fallback.
9. std::move and elision: interaction and anti-patterns
std::move only casts to an rvalue; it does not “enable elision” for named locals. For returned locals, it is usually the wrong tool.
Anti-pattern
Widget make() {
Widget w;
// ...
return std::move(w); // often blocks NRVO; forces move
}
Preferred
Widget make() {
Widget w;
// ...
return w;
}
When return std::move might be intended:
- You are not eliding and you need a move (e.g.
return std::move(member);in a special function—still mind moved-from state and lifetime). - A library API returns rvalue reference-like patterns (rare in idiomatic C++).
Rule of thumb — for local automatic objects returned by value, use plain return name; and let the compiler do NRVO or implicit move.
10. Function return: multiple return paths and NRVO
Multiple return statements do not by themselves preclude NRVO, but the compiler must be able to merge the “construct into return slot” plan across control flow.
Case that often breaks NRVO
std::vector<int> pick(bool f) {
std::vector<int> a{1}, b{2};
return f ? a : b; // two different return objects
}
Improving by returning prvalues (where T is cheap to construct) or a single buffer:
std::vector<int> pick(bool f) {
if (f) {
return std::vector<int>{1};
}
return std::vector<int>{2};
}
Or build one std::vector<int> v; and assign inside branches, then return v; when the compiler can see one returned object in all cases.
Pattern for status codes + data (without sacrificing clarity):
- Return
std::optional<T>,std::expected<T,E>, or a smallstruct { bool ok; T value; }—still prefer oneT valueto move out when successful.
11. Constructor elision: direct initialization
Constructor elision includes cases where a temporary is not materialized, such as
T x = T(T(T(42)));
(Under the right C++17 rules, this chain can collapse into direct initialization of x.)
Direct initialization in return (prvalue) lines up with guaranteed prvalue elision: you should think in terms of where the object is constructed, not “three temporaries and two moves”.
emplace-style construction (containers) and in-place construction (std::make_unique, std::vector::emplace_back) are different API surfaces but share the same philosophy: name the final storage once and construct there.
12. Pass by value: elision in parameters
C++ can construct a function parameter directly from the caller’s prvalue, avoiding an extra T in the caller:
void consume(Widget w);
/* caller */ consume(Widget(42));
A common mental diagram:
flowchart LR A[Call site prvalue] -->|Compiler maps ABI| B[Parameter slot] B --> C[Body uses w]
Contrast — T const& still avoids copies but does not transfer ownership. Pass by value + move is a pattern for sink parameters (take ownership) when the argument is a temporary.
C++11 idiom void sink(T&& t) with std::forward<T> in generic code is a different pattern (perfect forwarding) and is about universal references, not the same parameter elision story—do not conflate the two in templates.
13. Performance impact: what benchmarks usually show
Microbenchmarks with optimization enabled (-O2/-O3 or equivalent) almost always show:
- prvalue return — as fast as the minimal construction of
Tcan be. Elision has no O(n) extra pass over data for move; copy and move of large buffers still cost linear work when they actually run. - NRVO success — one construction, no
memcpy-likemoveof vector buffers. return std::move(local)on a type with heap storage — often a full move of pointers + counters every time, clearly slower than elision in tight loops (still far faster than deep copy if that were the alternative).
A toy mental picture, not a benchmark (always measure your type and ABI): for a million-int std::vector, RVO or NRVO is usually “allocate once, fill the buffer that the caller will own,” while a missed elision with move is still just swapping pointers and size—but it is extra work you did not need next to a successful elide. A big POD struct (think ~1 KiB) is harsher: if elision fails, you may pay something like a memcpy, and a copy is full O(n) in the data. So I do not treat “move” as the same as “elision” even when the move is “cheap.”
Takeaway — never pessimize by blocking NRVO; only if profiling proves it, restructure the function so that elision is possible again.
14. Debugging: observing elision and -fno-elide-constructors
GCC/Clang offer:
-fno-elide-constructors
Forcing the compiler to not elide copy/move in certain contexts, which makes copy / move visible in tracing logs.
Logger types (counts copies/moves):
struct Counted {
static int copies, moves;
Counted() = default;
Counted(Counted const&) { ++copies; }
Counted(Counted&&) noexcept { ++moves; }
};
Counted make() {
Counted c;
return c;
}
Without elision, you may see one move; with elision, often one default construction in the final home and no move.
ASan / UBSan and assembly inspection (Section 19) are complementary. Remember: in release mode, the optimizer may inline the constructor entirely—constructors disappear from the disassembly, which is a good sign, not a bug.
15. C++11 vs C++17 vs C++20: evolution of rules (high level)
- C++11 — Move semantics; return of locals is allowed to implicitly treat the operand as a move candidate in many return situations; NRVO and RVO still primarily described as permitted optimizations.
- C++17 — Prvalue materialization rules: guaranteed elision in specific initialization chains; the language no longer pretends that some copies must exist when they cannot in any sensible implementation. RVO for prvalues in
returnbecomes a semantic matter, not “maybe optimized away if lucky.” - C++20 — not a rewrite of elision, but related to value-category plumbing and more library features. For return optimization, keep thinking C++17+ prvalue rules (plus the usual NRVO implementation quality).
Practical upshot — if your codebase is C++14 or lower, even more reason not to std::move return locals, because implicit move fallbacks and RVO interplay differ in older specs and compiler support.
16. What I rely on (instead of a “best practices” checklist)
- I rely on prvalue + C++17 rules for factory functions:
return T{args...}. - I rely on mainstream NRVO for
T x; /* fill */ return x;in simple functions, but I do not bet correctness on mandatory NRVO the way I can for the guaranteed prvalue path. - I return by value for types designed with move (e.g.
std::string,std::vector); the pair (move, elision) is what makes value semantics feel “obvious” in modern C++. - I do not write
std::moveon returned locals to “be helpful.” - If a type is large and performance-critical, I still let MSVC / Clang / GCC have a say in CI: ABI and inlining can nudge the margins in ways a single-laptop Godbolt session will not.
17. Common mistakes (quick list)
return std::move(local);for automatic locals.- Returning a
T constand expecting move on fallback. - Conditional between two named locals: breaks NRVO (often).
- Assuming NRVO for return from lambda in every compiler/version—usually ok, but worth checking with tests when hot.
- Marking copy/move for logging in release and deciding based on never-inlined debug builds.
- Confusing
T&&in templates (forwarding) with return optimization in ordinary functions.
18. Real examples: factory functions and a builder
Factory (prvalue, excellent)
class User {
std::string name_;
int id_{0};
public:
User(std::string n, int i) : name_(std::move(n)), id_(i) {}
static User make_guest() {
return User{"guest", 0};
}
};
Builder (named, NRVO-friendly)
std::string build_csv(const std::vector<std::string>& fields) {
std::string out;
out.reserve(fields.size() * 16);
for (const auto& f : fields) {
if (!out.empty()) out += ',';
out += f;
}
return out; // NRVO typical
}
Deep hierarchy factory (avoid double work)
struct Config { /* ... */ };
Config load() {
Config c;
// ... populate ...
return c; // prefer NRVO over wrapping in std::move
}
19. Assembly analysis and Compiler Explorer (Godbolt)
Workflow
- Write the minimal hot return idiom.
- Open Compiler Explorer with the three compilers and
-O2. - Compare the number of calls to constructor, move, or memmove-like thunks.
- Toggle inlining (split TU vs header-only) to see how much disappears.
What “good” looks like for return Widget{}; into Widget w = f(); — a single construction path into w’s address (often via hidden pointer; ABI-dependent), not a move with two Widget complete lifetimes in separate stack slots.
A trivial Widget is often fully inlined; a vector-like Widget with heap pointer should show pointer moves only when move actually happens.
20. When something looks wrong (no lookup table, just what I do)
Extra moves in my instrumented return: I first check whether I have one clear returned object, then whether I smuggled in return std::move(x); for a local. I compare default optimize (-O2 / /O2) to the toy -fno-elide-constructors build when I need to count constructors—debug without inline is a lousy place to decide “how fast production is.”
Copies on return in older code: I look for a const return type or a type with no viable move; fixing the signature or adding a proper move ctor is usually cheaper than “clever” returns.
Branches that return two different names: I stop expecting NRVO, then either unify into one buffer, return prvalues per arm, or accept the move and see if the profile still cares.
MSVC looks different from Clang: I check /std and version before I panic; calling conventions and return registers are real, and a toolchain bump sometimes explains the last 5% I was chasing.
Debug builds look huge and slow: iterator debugging levels, debug allocators, and disabled elision can lie to you about the hot path. I judge release-shaped builds when I am arguing about RVO, not -O0 in isolation.
Lambda vs free function in microbenchmarks: I remember ABI and capture layout before I declare NRVO “broken”; a static free function is often a fairer A/B for “what would this look like in a header-y hot path.”
21. Out-parameters vs. return by value: when to prefer what
Return by value is the default style for C++ for owning results when move + elision apply. Out-parameters (void f(T& out) or T& f(T& out)) still matter when:
- You are reusing a buffer across a loop to avoid reallocation, and the caller controls lifetime (e.g.
std::vector<int>& scratch). - You need error codes in the classic C or COM style without an optional type.
- You return non-owning views (
string_view); those are not the same RVO object.
Anti-pattern to avoid — forcing an out-parameter only because you “heard copies are bad.” Modern C++ prefers value returns for new objects.
// Prefer (idiomatic, elision + move)
std::string build() {
std::string s;
s.append("x");
return s;
}
// Out-parameter: only if caller truly reuses the buffer every iteration
void build_into(std::string& out) {
out.clear();
out.append("x");
}
22. Exception safety, side effects, and elision (informal)
Copy elision can change the number of invocations of move/copy constructors that have observable side effects; therefore the Standard does not always permit a compiler to elide when it would change meaning. Guaranteed prvalue elision in C++17+ is a semantics change: there is no “extra” temporary. For debug logging in copy/move, rely on fno-elide builds when you must count invocations, not on a single -O0 vs -O2 comparison without reading the disassembly.
23. volatile and return (edge case)
A volatile local is rare in modern C++, but a volatile complete object is not a normal move candidate in the way a regular automatic object is. If you must return such an object, expect pessimization and portability questions—usually the fix is a different API (buffer + length, span, or non-volatile data with atomics).
24. [[nodiscard]] and [[maybe_unused]] (API hygiene)
[[nodiscard]] does not change elision, but it prevents silent discards of expensive return values, which in turn keeps NRVO benefits reachable in real code (someone must bind the return).
[[nodiscard]] std::vector<int> compute();
void oops() {
compute(); // warning: nodiscard
}
[[maybe_unused]] is for parameters; do not use it to “hide” expensive returns you did not need.
25. C++20 niceties (without changing the core RVO model)
- Designated initializers make large aggregate construction clearer at the call site; the elision story is still: prvalue when you
return S{.a=1,.b=2};and bind it—think C++17 + readability. - Concepts and constraints do not alter RVO; they only restrict which
Tyou instantiate. - Coroutines (
co_return) are a separate machinery (promise type, awaitables). Do not assume the same NRVO mental model for everyco_awaitpipeline—profile coroutine hot paths on your compiler.
26. std::array, small buffers, and SSO (short string optimization)
std::string SSO and small inline storage in custom types mean “move is cheap” but elision is still cheaper (no pointer or char shuffling, no destructor pairing). Example:
- Elided return — the string’s inline buffer is filled in place in the final object.
- Move return — a few integer/pointer operations + source left in a valid moved-from state.
Microbenchmarks are where I remind myself the numbers are order-of-magnitude and machine-specific (glibc vs jemalloc vs Windows heap, ASLR, all of it). An illustrative run on one desktop, -O2, loop a million times: a short std::string prvalue return is often fully inlined into nothing interesting in the assembly. A named std::string s with return s; is usually the same happy path on GCC/Clang as the prvalue story for small SSO-sized strings. A std::vector<int> of ~1000 elements usually shows one heap allocation in the callee either way if NRVO is doing its job. The line I watch is return std::move(s) when a plain return s; would do: the move of three pointers is “cheap” but pointless next to an elide, and in instrumented code it shows up in the counts. Always re-measure for your allocator and your binary.
27. Inlining, LTO, and why “no move in assembly” is subtle
Link-Time Optimization (LTO) and -flto=thin (Clang) or /LTCG (MSVC) can merge the callee into the caller, which removes the ABI-visible “return slot” in the disassembly you expected. The absence of a named T::~T in one TU does not mean NRVO failed—it may have been inlined out.
Heuristic — compare instruction count and load/store to the heap, not the presence of a single symbol.
28. std::optional, std::expected, and value returns
When modeling no value or error, prefer standard vocabulary types; still return the heavy T by move or in situ in the success path:
std::optional<std::vector<int>> parse() {
std::vector<int> v;
if (!ok()) return std::nullopt;
v.push_back(1);
return v; // NRVO or move into optional storage
}
std::optional in place construction (std::in_place_t) is available, but the return local idiom is usually clearer.
29. The “two stacks” mental model (pedagogical, not a standard term)
- The callee would have constructed
Tin its frame and then moved to the caller’s frame. - Elision lets the compiler use one storage address (the return slot the ABI reserves, or object storage at the call site), so step 1’s temporary never exists.
When you see RVO in a debugger, the object address in f() may match the address in main() in optimized code—that is the smoking gun, but debug builds can obfuscate it.
30. [[gnu::noinline]] / __attribute__((noinline)) for tests only
Deliberately forbidding inlining is useful in a laboratory main.cpp to watch move/copy, but is not a production style. In production, you want inlining; hiding it just to count is misleading for performance conclusions.
31. std::move of xvalues and returned members
Returning a member by value is not NRVO in the local sense; std::move may be appropriate when the object will not be used afterward:
struct S { Widget w; Widget take() { return std::move(w); } };
Do not confuse this with return local;—the local is *this context, but the operand is a data member with different lifetime rules. Review [[class.copy.elision]] and the move-from object invariants in your Widget type.
32. Standardese pointers (read once, then return to idioms)
The normative rules live under [class.temporary], [class.copy.elision], and [over.match.funcs]-adjacent initialization (depending on the Standard revision you cite). Copy elision is a permitted optimization in some historical modes; C++17 mandates certain prvalue paths to not create temporaries. As an engineer, the idioms in this post matter more than clause numbers, until you litigate a compiler bug in the bug tracker.
33. A few g++ / clang / MSVC flags I actually reach for (non-exhaustive)
-O0: Great for stepping in a debugger, bad for inferring RVO. Elision and inlining may barely happen; I do not use it to settle performance arguments.-O2/-O3: This is the neighborhood where I read Godbolt and microbenchmarks.-fno-elide-constructors(GCC, Clang): I flip this on instrumented types when I want copies and moves to show up in traces so I can see what the abstract machine is doing. It is a lab switch, not a production flag.-fno-inline(testing only): Makes calls visible in assembly; it also distorts what a real hot path looks like after inlining, so I treat it as a microscope, not a speedometer./O2on MSVC: Usual “release” neighbor; I pair it with the same/stdI ship with when I compare to Clang/GCC.
34. clang-tidy and static analysis (misc ideas)
Some clang-tidy checks and compiler warnings (e.g. redundant std::move on a value already treated as a prvalue in a return in an older Standard) may nudge you. The reliable human rule: for automatic locals, never return std::move(local);.
35. constexpr and constinit (compile-time not runtime elision)
constexpr functions must be usable in constant evaluation; RVO in constexpr exists as “no extra work,” but the story is about translation-time evaluation—different from CPU-level microbenchmarks at runtime. constinit is about static initialization, not the same return pipeline.
36. P0718R2 mental note (relocation, not required reading)
There has been committee discussion and vendor extensions around trivial relocation; until / unless your toolchain and type explicitly support a trivial reloc (future direction), you still code to C++17-era rvalue + move rules. Do not build production logic on a draft paper’s name alone.
37. “Copy from prvalue in two steps” in older textbooks
Pre-C++11 material sometimes shows a copy in T t = f(); as “copy from returned temporary.” Today, first look for prvalue and C++17 guarantees; second, NRVO; last, move or copy—the pedagogical two-step copy is often outdated for modern compilers and standards.
38. Widget const parameters and by-value sink
void take(Widget const w) { /* w is a copy! */ } // usually wrong for expensive types
void take(Widget w) { /* move or elide at call */ } // typical sink
Pass-by-value sink and RVO at the call site (Section 12) are separate from return NRVO but together they define end-to-end value-type pipelines.
39. Re-checklist before merging a performance PR
- Is every hot return either a prvalue or a single named local of the same type with simple control flow?
- Is there no
return std::move(local);for automatic locals? - Is LTO / PGO used consistently between dev and bench machines when claiming speedups?
- For portable performance, did you try at least Clang and MSVC on a vector-like type?
40. Longer “Godbolt” narrative example (pseudocode steps)
- Paste a tiny
SwithS(),S(S&&) noexceptprinting"move", and a defaulted copy if needed. - Branch A:
S f() { S x; return x; }inmain: S s = f(); - Branch B: same but
return std::move(x); - Count the
"move"prints with-O2and without-fno-elide-constructors—A should print not a move, B often prints a move in instrumented types when NRVO is blocked.
This 5-minute lab solidifies the anti-std::move rule in muscle memory.
Mermaid: decision overview
The following example demonstrates a compact mental model in mermaid:
graph TD
A[Function Return] --> B{Return Form}
B -->|return Type...| C[RVO]
C --> D[C++17 guaranteed prvalue path]
D --> E[0 copy or move in many chains]
B -->|return obj| F{Single named local, same type?}
F -->|Yes, simple path| G[NRVO attempt]
F -->|No / multiple| H[Move or copy]
G -->|Compiler OK| E
G -->|Not OK| I[Implicit move of local in return]
H --> I
Related posts
- Copy elision
- Return statement
- Move semantics
- Value categories
Keywords
C++, RVO, NRVO, copy elision, C++17, optimization, return value optimization, move semantics, NRVO, prvalue, guaranteed copy elision