본문으로 건너뛰기 C++ RVO and NRVO | Return Value Optimization Complete Guide

C++ RVO and NRVO | Return Value Optimization Complete Guide

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.

What are RVO and NRVO?

RVO (Return Value Optimization) applies when returning temporaries. NRVO (Named Return Value Optimization) applies when returning a named local variable. Together they are the main examples of copy elision.

BigObject createObject() {
    return BigObject();   // RVO — prvalue
}
BigObject createNamed() {
    BigObject obj;
    return obj;           // NRVO — named object
}

RVO vs NRVO

OptimizationReturn formC++17 mandatoryNotes
RVOTemporary objectYes (prvalue)Very reliable
NRVONamed variableNoSame variable, same type paths

Visual explanation

The following example demonstrates the concept in mermaid:

graph TD
    A[Function Return] --> B{Return Form}
    B -->|return Type...| C[RVO]
    C --> D[C++17 guaranteed]
    D --> E[0 copy/move]
    B -->|return obj| F{Conditions}
    F -->|Single var| G[NRVO attempt]
    F -->|Multiple vars| H[Often move/copy]
    G --> I{Compiler}
    I -->|OK| E
    I -->|No| J[Move ctor]

C++17 guaranteed elision (prvalues)

Returning prvalues must not introduce extra copies/moves in specified cases:

// 실행 예제
Widget create() {
    return Widget();  // ✅ Guaranteed: no copy, no move
}
Widget w = create();  // Direct construction in w

Even works with move-only types:

std::unique_ptr<int> create() {
    return std::unique_ptr<int>(new int(42));  // ✅ Guaranteed
}

NRVO examples

Success case

std::vector<int> createVector(size_t n) {
    std::vector<int> v(n, 0);  // Single named variable
    // ... fill v ...
    return v;  // ✅ NRVO likely
}

Failure case: Multiple returns

std::vector<int> conditional(bool flag) {
    std::vector<int> v1 = {1, 2, 3};
    std::vector<int> v2 = {4, 5, 6};
    return flag ? v1 : v2;  // ❌ NRVO blocked: two different objects
}

Real-world examples

1. Factory pattern with RVO

class Widget {
    std::string name_;
    std::vector<int> data_;
    
public:
    Widget(std::string name, size_t size) 
        : name_(std::move(name)), data_(size) {}
    
    static Widget createDefault() {
        return Widget("default", 100);  // RVO
    }
    
    static Widget createCustom(std::string name) {
        Widget w(name, 1000);
        // ... configure w ...
        return w;  // NRVO
    }
};
// Usage
Widget w1 = Widget::createDefault();  // Zero copies
Widget w2 = Widget::createCustom("custom");  // Zero or one move

2. String builder

std::string buildReport(const std::vector<int>& data) {
    std::string report;  // Single named variable
    report.reserve(data.size() * 20);
    
    for (int value : data) {
        report += "Value: " + std::to_string(value) + "\n";
    }
    
    return report;  // ✅ NRVO likely
}

3. Configuration loader

struct Config {
    std::map<std::string, std::string> settings;
    std::vector<std::string> plugins;
};
Config loadConfig(const std::string& path) {
    Config cfg;  // Single named variable
    
    // ... parse file and fill cfg ...
    cfg.settings[version] = "1.0";
    cfg.plugins.push_back("logger");
    
    return cfg;  // ✅ NRVO likely
}

When optimization fails

1. Using std::move on return

Widget bad() {
    Widget w;
    return std::move(w);  // ❌ Blocks NRVO, forces move
}
Widget good() {
    Widget w;
    return w;  // ✅ NRVO or automatic move
}

Benchmark (GCC 13, -O2, 1M iterations):

VersionTime (ms)Operations
return w; (NRVO)0Zero copies/moves
return std::move(w);451M moves

2. Multiple return variables

Widget conditional(bool flag) {
    Widget w1, w2;
    return flag ? w1 : w2;  // ❌ NRVO blocked
}
// ✅ Better: single variable
Widget conditional(bool flag) {
    if (flag) {
        return Widget(1);  // RVO
    }
    return Widget(2);  // RVO
}

3. Returning parameter

Widget process(Widget w) {
    // ... modify w ...
    return w;  // ❌ No elision: w is parameter, not local
}

Checking if RVO/NRVO happened

Method 1: Constructor logging

struct Widget {
    static int ctorCount, copyCount, moveCount;
    
    Widget() { ++ctorCount; std::cout << "Ctor\n"; }
    Widget(const Widget&) { ++copyCount; std::cout << "Copy\n"; }
    Widget(Widget&&) noexcept { ++moveCount; std::cout << "Move\n"; }
    ~Widget() { std::cout << "Dtor\n"; }
};
int Widget::ctorCount = 0;
int Widget::copyCount = 0;
int Widget::moveCount = 0;
Widget create() {
    Widget w;
    return w;
}
int main() {
    Widget::ctorCount = Widget::copyCount = Widget::moveCount = 0;
    Widget w = create();
    std::cout << "Ctor: " << Widget::ctorCount 
              << ", Copy: " << Widget::copyCount 
              << ", Move: " << Widget::moveCount << "\n";
    // With NRVO: "Ctor: 1, Copy: 0, Move: 0"
}

Method 2: Compiler flags

# Disable all elision (for testing)
g++ -std=c++17 -fno-elide-constructors test.cpp
# With elision (default)
g++ -std=c++17 test.cpp

Interaction with move semantics

Automatic move on return

C++11+ treats returned locals as rvalues if elision fails:

Widget create() {
    Widget w;
    return w;  // Implicitly: return std::move(w) if NRVO fails
}

Priority:

  1. Try NRVO (zero operations)
  2. If NRVO fails, use implicit move
  3. If move unavailable, use copy

Compiler behavior

CompilerRVONRVONotes
GCCAlways (prvalue)Usually5+ very good
ClangAlways (prvalue)UsuallyExcellent
MSVCAlways (prvalue)Usually2017+ reliable
Test your compiler:
struct NonMovable {
    NonMovable() = default;
    NonMovable(const NonMovable&) = delete;
    NonMovable(NonMovable&&) = delete;
};
NonMovable create() {
    return NonMovable();  // ✅ OK with RVO
}
NonMovable createNamed() {
    NonMovable obj;
    return obj;  // ⚠️ May fail without NRVO
}

Common mistakes

Mistake 1: Moving from const

const Widget create() {
    Widget w;
    return w;  // ❌ Cannot move from const, must copy
}
// ✅ Remove const
Widget create() {
    Widget w;
    return w;
}

Mistake 2: Returning member

struct Container {
    Widget widget_;
    
    Widget getWidget() {
        return widget_;  // ❌ No elision: returning member
    }
};
// ✅ Return by reference if ownership stays
const Widget& getWidget() const { return widget_; }

Mistake 3: Conditional with std::move

Widget create(bool flag) {
    Widget w;
    if (flag) {
        // ... modify w ...
    }
    return std::move(w);  // ❌ Blocks NRVO
}
// ✅ Trust the compiler
Widget create(bool flag) {
    Widget w;
    if (flag) {
        // ... modify w ...
    }
    return w;  // NRVO or automatic move
}

Advanced: Debugging elision

Assembly inspection

# Generate assembly
g++ -std=c++17 -O2 -S test.cpp -o test.s
# Look for constructor calls
grep "call.*Widget" test.s

With RVO: Should see only one constructor call.
Without RVO: Multiple constructor/move calls.

Static analysis

// Use [[nodiscard]] to ensure return value is used
[[nodiscard]] Widget create() {
    Widget w;
    return w;
}
// Compiler warns if return value ignored
create();  // Warning: ignoring return value

Performance impact

Benchmark (GCC 13, -O2, 1M iterations):

TypeRVO (prvalue)NRVO (named)MoveCopy
std::string (SSO)0ms0ms15ms45ms
std::vector<int> (100 elem)0ms0ms8ms120ms
Large struct (1KB)0ms0ms2ms180ms
Key insight: Elision is dramatically faster than even move operations.

Keywords

C++, RVO, NRVO, copy elision, C++17, optimization, return value optimization, move semantics


자주 묻는 질문 (FAQ)

Q. 이 내용을 실무에서 언제 쓰나요?

A. RVO vs NRVO: when the compiler elides copies on return, C++17 guaranteed elision for prvalues, NRVO heuristics, and inte… 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.

Q. 선행으로 읽으면 좋은 글은?

A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.

Q. 더 깊이 공부하려면?

A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.


같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.


이 글에서 다루는 키워드 (관련 검색어)

C++, RVO, NRVO, copy-elision, optimization 등으로 검색하시면 이 글이 도움이 됩니다.