[2026] C++ Copy Elision | When Copies and Moves Disappear

[2026] 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

다음은 간단한 cpp 코드 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

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

아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

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)

아래 코드는 cpp를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

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)

아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

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

다음은 간단한 cpp 코드 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

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

Fix: Return prvalue or use single object: 아래 코드는 cpp를 사용한 구현 예제입니다. 조건문으로 분기 처리를 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

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

2. Returning parameter

다음은 간단한 cpp 코드 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

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

아래 코드는 cpp를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 코드를 직접 실행해보면서 동작을 확인해보세요.

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

아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

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:
아래 코드는 cpp를 사용한 구현 예제입니다. 조건문으로 분기 처리를 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
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

아래 코드는 cpp를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 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)

아래 코드는 cpp를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 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

다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 에러 처리를 통해 안정성을 확보합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

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

아래 코드는 bash를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

# 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

아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// 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: 다음은 간단한 cpp 코드 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

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): 아래 코드는 cpp를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 코드를 직접 실행해보면서 동작을 확인해보세요.

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

After (C++17): 아래 코드는 cpp를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 코드를 직접 실행해보면서 동작을 확인해보세요.

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

Example: Builder pattern

다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

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

... 996 lines not shown ... Token usage: 63706/1000000; 936294 remaining Start-Sleep -Seconds 3