Question

Copy Elision and Return Value Optimization in C++

cppoptimizationc++-faqreturn-value-optimizationcopy-elision

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:

  1. Construct a local or temporary object.
  2. Copy or move it into the function's return object.
  3. 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.

Take Quiz

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:

  1. main calls createTag().
  2. The return expression creates a std::string prvalue: std::string{"release"}.
  3. 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.
  4. tag is initialized from the function result. Implementations can construct the final tag object directly as part of this process.
  5. When main ends, the one resulting std::string is 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.

Take Quiz

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

SituationExampleCopy/move guaranteed to be avoided in C++17?Notes
Return an unnamed prvaluereturn Widget{};Yes, when the types matchCommon guaranteed-elision case.
Return a named localWidget w; return w;NoNRVO is permitted; a move is the usual fallback.
Return a different local on each branchif (ok) return a; return b;NoNRVO is harder or unavailable; the type should normally be movable.
Return a parameterWidget f(Widget w) { return w; }NoThe parameter is not an NRVO candidate; it may be moved.
Explicitly move a localreturn std::move(w);NoOften 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.
Take Quiz

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;, not return 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.

Take Quiz

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

  1. Define a Report type with a title and a list of numeric values.
  2. Create a function that returns a default report using an unnamed Report object.
  3. Create a function that builds a filtered report in a named local variable and returns it normally.
  4. Print the returned reports in main.
  5. Do not use new, delete, output parameters, or std::move in the return statements.
Take Quiz

Keep learning

Related questions

View more