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):
| Version | Time (ms) | Operations |
|---|---|---|
return w; (NRVO) | 0 | Zero copies/moves |
return std::move(w); | 45 | 1M 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:
- Try NRVO (zero operations)
- If NRVO fails, use implicit move
- 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
| 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
- RVO/NRVO
- Move semantics
- Return statement optimization
- Value categories
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++ RVO/NRVO | ‘Return Value Optimization’ 가이드
- C++ Return Statement | ‘반환문’ 가이드
- C++ Copy Initialization | ‘복사 초기화’ 가이드
이 글에서 다루는 키워드 (관련 검색어)
C++, copy-elision, optimization, C++17 등으로 검색하시면 이 글이 도움이 됩니다.