What is NRVO in C++ and When Can Compilers Omit It?

Named Return Value Optimization (NRVO) is a compiler optimization technique in C++ that eliminates the overhead of copying or moving a named local object when returning it by value from a function. This article explains how NRVO works, how it differs from mandatory copy elision, and the specific circumstances where a C++ compiler is legally permitted or practically required to omit this optimization.

What is Named Return Value Optimization (NRVO)?

In C++, returning an object by value traditionally requires the object to be constructed inside the function's local stack frame and then copied or moved into the caller's destination storage before the local object is destroyed.

NRVO is a form of copy elision. When applied, the compiler constructs the named local object directly inside the memory space allocated for the caller's return value. This eliminates both the copy/move constructor call and the corresponding destructor call, resulting in zero-cost value returns and improved performance.

struct Heavy {
    Heavy() { std::cout << "Constructed\n"; }
    Heavy(const Heavy&) { std::cout << "Copied\n"; }
    Heavy(Heavy&&) noexcept { std::cout << "Moved\n"; }
    ~Heavy() { std::cout << "Destroyed\n"; }
};

Heavy createHeavy() {
    Heavy obj; // With NRVO, constructed directly in caller's memory
    // Perform operations on obj
    return obj; // Neither copy nor move is executed
}

NRVO vs. Regular RVO

It is important to distinguish between Return Value Optimization (RVO) and Named Return Value Optimization (NRVO):

  • RVO (Unnamed Return Value Optimization): Applies to unnamed temporary expressions (prvalues), such as return Heavy{};. Since C++17, this is no longer an optimization; it is a guaranteed language rule known as mandatory copy elision.
  • NRVO (Named Return Value Optimization): Applies to named local variables with automatic storage duration, such as Heavy obj; return obj;. Unlike prvalue elision, NRVO is strictly optional under the C++ standard. Compilers are allowed, but never mandated, to perform it.

When Is the Compiler Allowed to Omit NRVO?

Because the C++ standard classifies NRVO as an optional optimization rather than a language requirement, compilers are permitted to omit it at any time, including when optimization flags are disabled (e.g., -O0 in GCC and Clang, or /Od in MSVC).

Beyond compilation flags, the compiler is structurally prevented or explicitly allowed to omit NRVO in the following scenarios:

1. Multiple Return Paths with Different Variables

If a function contains multiple return statements returning different named variables, the compiler cannot easily determine at compile time which object will inhabit the caller's memory frame. In such cases, compilers almost universally omit NRVO:

Heavy choose(bool condition) {
    Heavy a;
    Heavy b;
    if (condition) {
        return a; // Different local objects returned
    }
    return b;     // NRVO typically omitted; fallbacks to move
}

2. Returning Function Parameters

NRVO only applies to local variables declared inside the function body with automatic storage duration. Parameters passed by value cannot undergo NRVO because their storage is already allocated as part of the parameter passing convention:

Heavy process(Heavy param) {
    // Operations on param...
    return param; // NRVO is disallowed here; move elision or move constructor is used instead
}

3. Explicit Use of std::move

Applying std::move to a local variable in a return statement inhibits NRVO. The expression return std::move(obj); yields an rvalue reference (Heavy&&) rather than a named variable of type Heavy, forcing a move constructor call instead of eliding it:

Heavy badPractice() {
    Heavy obj;
    return std::move(obj); // Prevents NRVO completely
}

4. Returning Global, Static, or Member Variables

NRVO is restricted exclusively to local objects with automatic storage duration whose lifetimes end with the function. If a function returns an object with static, thread-local, or dynamic storage duration, or a member of another class, the compiler cannot elide the copy or move:

Heavy globalObj;

Heavy getGlobal() {
    return globalObj; // NRVO not permitted; object must outlive the call
}

5. Type Conversions

NRVO requires the returned variable's type to match the function's return type exactly (ignoring top-level cv-qualifiers). If an implicit conversion or inheritance upcasting is required, NRVO is prohibited:

struct Base {};
struct Derived : Base {};

Base getDerived() {
    Derived d;
    return d; // NRVO cannot occur because the types do not match; conversion required
}

6. Volatile-Qualified Objects

If the returned object is qualified as volatile, the C++ standard specifies that the compiler must honor every read and write to that memory location. Consequently, copy elision and NRVO are not permitted on volatile objects.