[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):
| Version | Time (ms) | Operations |
|---|---|---|
return w; (NRVO) | 0 | Zero copies/moves |
return std::move(w); | 45 | 1M 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:
- Try NRVO (zero operations)
- If NRVO fails, use implicit move
- 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
| Compiler | C++17 guaranteed elision | NRVO |
|---|---|---|
| GCC | 7+ | 3+ (partial), 5+ (good) |
| Clang | 4+ | 3+ |
| MSVC | 2017 15.5+ | 2015+ |
| Note: NRVO has been supported for decades, but C++17 made prvalue elision mandatory. |
Related posts
Keywords
C++, copy elision, RVO, NRVO, C++17, optimization, move semantics, prvalue, guaranteed elision