Question
Copy Elision and Return Value Optimization in C++
Question
What is copy elision in C++? What are return value optimization (RVO) and named return value optimization (NRVO), and what do they imply for constructors, copies, and moves?
In which situations can these optimizations occur, and what limitations or rules should developers understand?
Short Answer
Copy elision is a C++ rule that lets the compiler construct an object directly in its final destination instead of creating a temporary and then copying or moving it. RVO applies when returning an unnamed temporary; NRVO applies when returning a named local object. Since C++17, several common temporary-return cases are guaranteed to avoid copies or moves, while NRVO remains an optimization that compilers may choose to perform.
Concept
Copying and moving objects can be expensive or can produce observable side effects, such as logging from a copy constructor. Copy elision allows C++ to skip creating an intermediate object when that object would immediately be copied or moved into another object.
The most familiar forms are:
- RVO (Return Value Optimization): a function returns an unnamed object, such as
return Report{};. - NRVO (Named Return Value Optimization): a function returns one named local object, such as
return report;.
Without elision, a return might conceptually involve these stages:
- Construct a local or temporary object.
- Copy or move it into the function's return object.
- Possibly copy or move it again into the caller's variable.
With elision, the implementation can construct one object directly in the caller's destination.
Report makeReport() {
return Report{"daily"}; // RVO: construct directly in the result destination
}
This matters because functions can return objects naturally by value without requiring manual memory management or output parameters merely to avoid copies.
C++17 and guaranteed elision
Before C++17, both RVO and NRVO were permitted optimizations. A compiler was allowed, but not required, to omit copies or moves.
Since C++17, when a function returns a prvalue of the same type as its return type, such as return Report{};, no separate temporary result object is materialized. This is often described as guaranteed copy elision.
Report makeReport() {
return Report{}; // Works in C++17 even if Report is neither copyable nor movable.
}
NRVO is different: returning a named local, return report;, is still not guaranteed by the language. Compilers commonly perform it, but code must remain valid if it does not happen.
Mental Model
Think of an object being returned as a package sent from a workshop to a customer.
- Without copy elision, the workshop builds a package on a table, repacks it into a delivery box, and then the customer may repack it again.
- With RVO, the workshop builds the package directly in the customer's delivery box.
- With NRVO, the workshop has already given the package a label (
report) but can still build that labelled package directly in the customer's box.
The important distinction is that C++17 requires direct construction in certain unnamed-temporary cases, but direct construction for a named local is usually available rather than promised.
Syntax and Examples
Core patterns
RVO: return an unnamed object
#include <string>
class Message {
public:
explicit Message(std::string text) : text_(std::move(text)) {}
private:
std::string text_;
};
Message makeWelcomeMessage() {
return Message{"Welcome"};
}
Message{"Welcome"} is an unnamed temporary. In C++17 and later, it is constructed directly as the function result when the types match.
NRVO: return one named local object
#include <string>
std::string makeGreeting(const std::string& name) {
std::string greeting = "Hello, " + name;
return greeting;
}
greeting is a named local variable. The compiler can apply NRVO and construct it directly in the caller's result storage. Do not write return std::move(greeting); merely to "help"; it can prevent NRVO.
Returning a value at the call site
std::string greeting = makeGreeting("Ada");
When NRVO occurs, greeting is effectively constructed where it will live, rather than receiving a copied or moved intermediate string.
Step by Step Execution
Consider this C++17 example:
#include <string>
std::string createTag() {
return std::string{"release"};
}
int main() {
std::string tag = createTag();
}
Step by step:
maincallscreateTag().- The return expression creates a
std::stringprvalue:std::string{"release"}. - Because the returned prvalue has the same type as the function return type, C++17 constructs the result directly from that expression. It does not first need a separate temporary string that is moved into the return value.
tagis initialized from the function result. Implementations can construct the finaltagobject directly as part of this process.- When
mainends, the one resultingstd::stringis destroyed.
Now compare a named local:
std::string createTag() {
std::string tag = "release";
return tag;
}
Here the compiler may apply NRVO, making tag and the caller's result the same object. If NRVO is not applied, C++ normally tries to move tag because it is a local object being returned; it copies only if moving is unavailable or unsuitable.
Real World Use Cases
Copy elision makes value-returning interfaces practical and expressive.
-
Factory functions: return a fully configured object.
Connection openConnection(Config config) { return Connection{std::move(config)}; } -
Parsing: return a parsed data structure by value.
Document parseDocument(std::string_view input) { return Document{input}; } -
Data transformations: create and return a new collection or result object.
std::vector<int> positiveValues(const std::vector<int>& values) { std::vector<int> result; for (int value : values) { if (value > 0) result.push_back(value); } return result; } -
Error-aware APIs: return a value type that contains either a result or error details, such as
std::optional<T>or an application-specific result type.
The practical lesson is to prefer a clear return-by-value API when it represents ownership and lifetime correctly. Modern C++ is designed to make this efficient.
Real Codebase Usage
In production code, developers usually rely on copy elision without writing special code for it.
Prefer simple return expressions
User makeGuestUser() {
return User{"guest"};
}
This is clear and benefits from C++17 guaranteed elision when applicable.
Return a local normally
std::vector<Record> loadRecords() {
std::vector<Record> records;
// Fill records...
return records;
}
This allows NRVO. If NRVO is not performed, the vector is typically moved, which is generally cheap because it transfers ownership of its internal allocation.
Use guard clauses with value returns
#include <optional>
#include <string_view>
std::optional<int> parseSingleDigit(std::string_view text) {
if (text.size() != 1) {
return std::nullopt;
}
if (text[0] < '0' || text[0] > '9') {
return std::nullopt;
}
return text[0] - '0';
}
Each return expresses the result directly. The code is easier to read than managing a caller-owned output object.
Write types that can move when appropriate
NRVO is optional. For named local returns, a movable type provides an efficient fallback when a compiler cannot or does not apply NRVO. Types that own resources commonly define or use move operations, often through members such as std::vector, std::string, or smart pointers.
Common Mistakes
Adding std::move to a local return
This is a common mistake:
std::string makeName() {
std::string name = "Ada";
return std::move(name); // Usually avoid this
}
std::move(name) turns the expression into an xvalue and can stop NRVO from applying. Write this instead:
return name;
C++ will consider moving name if NRVO does not happen.
Assuming NRVO is guaranteed
class NonMovable {
public:
NonMovable() = default;
NonMovable(const NonMovable&) = delete;
NonMovable(NonMovable&&) = delete;
};
NonMovable build(bool useFirst) {
NonMovable first;
NonMovable second;
if (useFirst) {
return first; // NRVO cannot reliably cover both locals
}
return second;
}
This is not reliably valid because NRVO is optional and the function may need a move or copy fallback. Returning an unnamed prvalue can be a better design when possible:
NonMovable build() {
return NonMovable{};
}
Returning a reference to a local object
Copy elision does not make this safe:
const std::string& badName() {
std::string name = "Ada";
return name; // Broken: name is destroyed when the function returns
}
Return by value instead:
std::string goodName() {
return "Ada";
}
Expecting constructor logging to always appear
Copy elision is one of the few optimizations that may change observable constructor and destructor side effects. Do not use copy or move constructor logging as essential program logic. The number of constructor calls can differ by language version, compiler, and build settings.
Forgetting that a valid destructor is still required
Even in C++17 guaranteed-elision cases, the returned type's destructor must be accessible and non-deleted at the return statement. The object will eventually need to be destroyed.
Comparisons
| Situation | Example | Copy/move guaranteed to be avoided in C++17? | Notes |
|---|---|---|---|
| Return an unnamed prvalue | return Widget{}; | Yes, when the types match | Common guaranteed-elision case. |
| Return a named local | Widget w; return w; | No | NRVO is permitted; a move is the usual fallback. |
| Return a different local on each branch | if (ok) return a; return b; | No | NRVO is harder or unavailable; the type should normally be movable. |
| Return a parameter | Widget f(Widget w) { return w; } | No | The parameter is not an NRVO candidate; it may be moved. |
| Explicitly move a local | return std::move(w); | No | Often prevents NRVO; normally use return w;. |
Copy elision versus move semantics
- Copy elision avoids creating an extra source object and avoids the copy/move operation entirely.
- Move semantics transfers resources from an existing object into another object.
- Copy elision is better when available because there is no intermediate transfer, but move semantics remains an important fallback.
RVO versus NRVO
- RVO returns an unnamed object, for example
return Widget{};. - NRVO returns one named local object, for example
return widget;. - In modern C++, the former commonly has language-guaranteed direct construction; the latter remains an allowed optimization.
Cheat Sheet
-
Copy elision: omission of a copy or move by constructing an object directly in its destination.
-
RVO: return an unnamed object.
Widget makeWidget() { return Widget{}; } -
NRVO: return a named local object.
Widget makeWidget() { Widget widget; return widget; } -
In C++17+, returning a same-type prvalue such as
return Widget{};has guaranteed direct construction. -
NRVO is not guaranteed, even in C++17+.
-
Return a local as
return local;, notreturn std::move(local);. -
If NRVO does not apply, returning an eligible local generally uses its move constructor when available.
-
Returning a parameter is not NRVO.
-
Never return a reference or pointer to a local object.
-
A returned type still needs an accessible, non-deleted destructor.
FAQ
Is copy elision an optimization or a language rule?
It is both, depending on the case. Historically it was a permitted optimization. Since C++17, some prvalue initialization cases use language rules that guarantee no separate temporary object is created.
Is RVO guaranteed in C++?
A common RVO-style expression, return Type{};, is guaranteed in C++17 and later when the returned prvalue has the same type as the function's return type. Older C++ versions only permitted the optimization.
Is NRVO guaranteed in C++17?
No. return localVariable; can use NRVO, but the compiler is not required to do so. A movable return type is usually a good fallback.
Should I use std::move when returning a local variable?
Usually no. Use return localVariable;. The compiler can perform NRVO, or move the local if NRVO is not used. Explicit std::move can prevent NRVO.
Can a function return a non-copyable, non-movable type by value?
Yes, in C++17 and later, when the function returns an appropriate same-type prvalue, such as return Type{};. Returning a named local still requires care because NRVO is optional.
Does copy elision make returning large objects by value efficient?
Often, yes. RVO can avoid intermediate objects entirely, and move semantics is commonly an efficient fallback. Measure performance for unusual types or critical paths.
Why do my copy constructor log messages disappear?
The compiler may have elided the copy or move, so that constructor is never called. In C++17 guaranteed-elision cases, there is no separate object to copy or move.
Mini Project
Description
Build a small report factory that returns a value object by value. It demonstrates both an unnamed direct return and a named local return, while keeping the interface simple and safe.
Goal
Create report objects without manual allocation and return them using RVO- and NRVO-friendly code.
Requirements
- Define a
Reporttype with a title and a list of numeric values. - Create a function that returns a default report using an unnamed
Reportobject. - Create a function that builds a filtered report in a named local variable and returns it normally.
- Print the returned reports in
main. - Do not use
new,delete, output parameters, orstd::movein the return statements.
Keep learning
Related questions
Advantages of Brace Initialization in C++
Learn why C++ brace initialization is often clearer and safer than other object initialization styles, with examples and common pitfalls.
Basic Rules and Idioms for Operator Overloading in C++
Learn the core rules, syntax, and common idioms for operator overloading in C++, including member vs non-member operators.
C++ Aggregates, Trivial Types, Trivially Copyable Types, and PODs Explained
Learn what aggregates, trivial types, trivially copyable types, and PODs mean in C++, how they differ, and why they matter.