본문으로 건너뛰기
Previous
Next
C++ Copy Elision | When Copies and Moves Disappear

C++ Copy Elision | When Copies and Moves Disappear

C++ Copy Elision | When Copies and Moves Disappear

이 글의 핵심

Copy elision: RVO, NRVO, C++17 guaranteed elision for prvalues, parameter initialization, and why returning local variables with std::move often hurts.

Introduction

Copy elision removes unnecessary copy/move operations. Since C++17, certain prvalue flows must elide copies/moves as specified by the “guaranteed copy elision” rules.

Kinds of elision

RVO — return of prvalue

Widget create() {
    return Widget();  // prvalue
}

NRVO — named local

Widget create() {
    Widget w;
    return w;  // often elided; not always guaranteed like prvalue elision
}

Function arguments

void process(Widget w);
process(Widget());  // often constructs `w` directly

C++17 and prvalues

Returning prvalues and certain initializations must not introduce extra copy/move in the mandated cases—types may even be non-copyable if only moves/prvalue paths are used legally per standard rules.

Common mistakes

std::move on return of local

Widget bad() {
    Widget w;
    return std::move(w);  // often worse: can inhibit NRVO, forces move
}
Widget good() {
    Widget w;
    return w;
}

Multiple return objects

Returning different locals on different paths can prevent NRVO.

Compiler switches

-fno-elide-constructors (GCC/Clang) disables elision for debugging constructor traces.

Detailed examples with assembly

Example 1: Guaranteed RVO (C++17)

struct Widget {
    int data[100];
    Widget() { std::cout << "Constructed\n"; }
    Widget(const Widget&) { std::cout << "Copied\n"; }
    Widget(Widget&&) { std::cout << "Moved\n"; }
};
Widget create() {
    return Widget();  // Prvalue: guaranteed elision
}
int main() {
    Widget w = create();  // Direct construction in w's storage
}

Output:

Constructed

Assembly (GCC 13, -O2):

; create() receives pointer to return slot
; Constructs Widget directly there
; No copy/move instructions

Example 2: NRVO (not guaranteed)

Widget createNamed() {
    Widget w;
    // ....use w ...
    return w;  // Named: NRVO possible but not mandatory
}
int main() {
    Widget w = createNamed();
}

With NRVO (GCC/Clang -O2):

Constructed

Without NRVO (-fno-elide-constructors):

Constructed
Moved

When elision is blocked

1. Conditional returns

Widget conditional(bool flag) {
    Widget w1, w2;
    return flag ? w1 : w2;  // ❌ NRVO blocked: multiple return objects
}

Fix: Return prvalue or use single object:

Widget conditional(bool flag) {
    if (flag) {
        return Widget(1);  // RVO
    }
    return Widget(2);  // RVO
}

2. Returning parameter

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

Fix: If modification is needed, accept by value is fine. If not, accept by const reference and return new object.

3. Returning member

struct Container {
    Widget widget_;
    
    Widget get() {
        return widget_;  // ❌ No elision: returning member
    }
};

Fix: Return by reference if ownership stays with container:

const Widget& get() const { return widget_; }
Widget& get() { return widget_; }

The std::move anti-pattern

Why std::move on return is usually wrong

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
Exception: When returning a different object:
Widget process(Widget w, bool modify) {
    if (modify) {
        Widget result = transform(w);
        return result;  // NRVO possible
    }
    return std::move(w);  // ✅ OK: w is parameter, move is intentional
}

C++17 guaranteed elision rules

What’s guaranteed

// 1. Prvalue initialization
Widget w = Widget();  // ✅ Guaranteed
// 2. Prvalue return
Widget create() { return Widget(); }  // ✅ Guaranteed
Widget w = create();
// 3. Prvalue function argument
void process(Widget w);
process(Widget());  // ✅ Guaranteed
// 4. Temporary materialization
const Widget& ref = Widget();  // ✅ Guaranteed (lifetime extended)

What’s NOT guaranteed (but often happens)

// NRVO: Named return value optimization
Widget create() {
    Widget w;
    return w;  // ⚠️ Not guaranteed, but usually optimized
}
// Multiple return paths
Widget conditional(bool flag) {
    Widget w1, w2;
    return flag ? w1 : w2;  // ⚠️ Not guaranteed
}

Debugging elision

Method 1: Constructor logging

struct Widget {
    static int ctorCount, copyCount, moveCount;
    
    Widget() { ++ctorCount; }
    Widget(const Widget&) { ++copyCount; }
    Widget(Widget&&) noexcept { ++moveCount; }
    
    static void reset() {
        ctorCount = copyCount = moveCount = 0;
    }
    
    static void report() {
        std::cout << "Ctor: " << ctorCount 
                  << ", Copy: " << copyCount 
                  << ", Move: " << moveCount << "\n";
    }
};
int Widget::ctorCount = 0;
int Widget::copyCount = 0;
int Widget::moveCount = 0;
// Test
Widget::reset();
Widget w = create();
Widget::report();  // Should show: Ctor: 1, Copy: 0, Move: 0

Method 2: Compiler flags

# Disable elision to see all copies/moves
g++ -std=c++17 -fno-elide-constructors test.cpp
# Enable verbose optimization reports
g++ -std=c++17 -O2 -fopt-info-vec-optimized test.cpp

Method 3: Static analysis

// Use concepts to enforce move-only types
template<typename T>
concept MoveOnlyElisionSafe = std::movable<T> && !std::copyable<T>;
template<MoveOnlyElisionSafe T>
T create() {
    return T();  // Must use elision or move (no copy available)
}

Interaction with move semantics

Automatic move on return

C++11+ automatically treats returned locals as rvalues:

Widget create() {
    Widget w;
    return w;  // Implicitly treated as 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

Real-world impact

Example: Factory pattern

Before (C++03):

class Factory {
public:
    static std::unique_ptr<Widget> create() {
        return std::unique_ptr<Widget>(new Widget());
    }
};

After (C++17):

class Factory {
public:
    static Widget create() {
        return Widget();  // Guaranteed elision, no heap allocation needed
    }
};

Example: Builder pattern

class WidgetBuilder {
    Widget widget_;
    
public:
    WidgetBuilder& setSize(int size) {
        widget_.size = size;
        return *this;
    }
    
    WidgetBuilder& setColor(Color c) {
        widget_.color = c;
        return *this;
    }
    
    Widget build() && {
        return std::move(widget_);  // ✅ OK: builder is rvalue, move is explicit
    }
};
// Usage
Widget w = WidgetBuilder()
    .setSize(100)
    .setColor(Color::Red)
    .build();  // One construction, one move (or elision)

Compiler support

CompilerC++17 guaranteed elisionNRVO
GCC7+3+ (partial), 5+ (good)
Clang4+3+
MSVC2017 15.5+2015+
Note: NRVO has been supported for decades, but C++17 made prvalue elision mandatory.

Keywords

C++, copy elision, RVO, NRVO, C++17, optimization, move semantics, prvalue, guaranteed elision


자주 묻는 질문 (FAQ)

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

A. Copy elision: RVO, NRVO, C++17 guaranteed elision for prvalues, parameter initialization, and why returning local variab… 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.

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

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

Q. 더 깊이 공부하려면?

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


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

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


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

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