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
Keywords
C++, copy elision, RVO, NRVO, C++17, optimization, move semantics, prvalue, guaranteed elision
Frequently Asked Questions (FAQ)
Q. When would I use this in practice?
A. Copy elision: RVO, NRVO, C++17 guaranteed elision for prvalues, parameter initialization, and why returning local variab…
Q. What should I read before this?
A. Follow the previous article or related articles links at the bottom of each post to learn in sequence. See the C++ series index for the full picture.
Q. Where can I study this more deeply?
A. Check cppreference and the relevant library’s official documentation. The reference links at the end of the article are also worth using.
Related Articles (Internal Links)
Other articles related to this topic.
- C++ RVO/NRVO | ‘Return Value Optimization’ 가이드
- C++ Return Statement | ‘반환문’ 가이드
- C++ Copy Initialization | ‘복사 초기화’ 가이드
Keywords Covered in This Article (Related Search Terms)
This article covers C++, copy-elision, optimization, C++17.