[2026] C++ RVO·NRVO | 복사 생략 최적화와 성능 향상
이 글의 핵심
C++ RVO·NRVO의 C++, RVO·NRVO, 복사, 들어가며: return에 std::move를 써야 하나요?를 실전 예제와 함께 상세히 설명합니다.
들어가며: “return에 std::move를 써야 하나요?"
"함수 반환 시 복사가 일어나지 않아요”
C++에서 함수 반환 시 복사가 생략되는 것은 RVO(Return Value Optimization) 덕분입니다. 아래 코드는 cpp를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// ❌ std::move 불필요
std::string foo() {
std::string result = "Hello";
return std::move(result); // ❌ RVO 방해
}
// ✅ RVO 활용
std::string foo() {
std::string result = "Hello";
return result; // ✅ 복사 생략
}
이 글에서 다루는 것:
- RVO와 NRVO
- C++17 복사 생략 보장
- std::move 실수
- 성능 측정
실무에서 마주한 현실
개발을 배울 때는 모든 게 깔끔하고 이론적입니다. 하지만 실무는 다릅니다. 레거시 코드와 씨름하고, 급한 일정에 쫓기고, 예상치 못한 버그와 마주합니다. 이 글에서 다루는 내용도 처음엔 이론으로 배웠지만, 실제 프로젝트에 적용하면서 “아, 이래서 이렇게 설계하는구나” 하고 깨달은 것들입니다. 특히 기억에 남는 건 첫 프로젝트에서 겪은 시행착오입니다. 책에서 배운 대로 했는데 왜 안 되는지 몰라 며칠을 헤맸죠. 결국 선배 개발자의 코드 리뷰를 통해 문제를 발견했고, 그 과정에서 많은 걸 배웠습니다. 이 글에서는 이론뿐 아니라 실전에서 마주칠 수 있는 함정들과 해결 방법을 함께 다루겠습니다.
목차
1. RVO란?
RVO (Return Value Optimization)
RVO는 함수 반환 시 복사를 생략하는 최적화입니다.
struct Data {
std::vector<int> vec;
Data() {
std::cout << "Constructor\n";
}
Data(const Data&) {
std::cout << "Copy Constructor\n";
}
Data(Data&&) noexcept {
std::cout << "Move Constructor\n";
}
};
// RVO 예시
Data createData() {
return Data(); // 임시 객체 반환
}
int main() {
Data d = createData();
// 출력: Constructor (복사/이동 없음!)
}
RVO 조건:
- 임시 객체 (prvalue) 반환
- C++17부터 보장
2. NRVO란?
NRVO (Named Return Value Optimization)
NRVO는 이름 있는 지역 변수 반환 시 복사를 생략합니다.
// NRVO 예시
Data createData() {
Data result; // 이름 있는 변수
result.vec.push_back(42);
return result; // NRVO 가능
}
int main() {
Data d = createData();
// 출력: Constructor (복사/이동 없음!)
}
NRVO 조건:
- 이름 있는 지역 변수 반환
- 모든 return문이 같은 변수 반환
- C++17에서도 선택적 (보장 안 됨)
3. C++17 복사 생략 보장
C++17 이전: 선택적
아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// C++14
Data createData() {
return Data(); // RVO 가능 (선택적)
}
// 컴파일러가 RVO를 안 하면:
// 1. 이동 생성자 호출
// 2. 이동 생성자도 없으면 복사 생성자 호출
C++17 이후: 보장
다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// C++17
Data createData() {
return Data(); // RVO 보장
}
// 이동/복사 생성자가 없어도 OK
struct NonMovable {
NonMovable() = default;
NonMovable(const NonMovable&) = delete;
NonMovable(NonMovable&&) = delete;
};
NonMovable createNonMovable() {
return NonMovable(); // ✅ C++17: OK
}
4. std::move 실수
실수 1: return에 std::move
아래 코드는 cpp를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// ❌ RVO 방해
std::string foo() {
std::string result = "Hello";
return std::move(result); // ❌ RVO 불가
}
// ✅ RVO 활용
std::string foo() {
std::string result = "Hello";
return result; // ✅ NRVO 가능
}
이유: std::move는 rvalue 참조로 변환하므로 NRVO 조건 위반.
실수 2: 여러 return문
아래 코드는 cpp를 사용한 구현 예제입니다. 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// ❌ NRVO 불가
std::string foo(bool flag) {
std::string a = "A";
std::string b = "B";
if (flag) {
return a; // a 반환
} else {
return b; // b 반환 (다른 변수!)
}
}
// NRVO 조건 위반: 여러 변수 반환
// 이동 생성자 호출
실수 3: 참조 반환
아래 코드는 cpp를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// ❌ RVO 불가
std::string& foo() {
std::string result = "Hello";
return result; // ❌ 댕글링 참조
}
// ✅ 값 반환
std::string foo() {
std::string result = "Hello";
return result; // ✅ NRVO 가능
}
5. 성능 측정
벤치마크
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <benchmark/benchmark.h>
struct Data {
std::vector<int> vec;
Data() : vec(1000000, 42) {}
};
// RVO
static void BM_RVO(benchmark::State& state) {
for (auto _ : state) {
Data d = { return Data(); }();
benchmark::DoNotOptimize(d);
}
}
BENCHMARK(BM_RVO);
// std::move (RVO 방해)
static void BM_Move(benchmark::State& state) {
for (auto _ : state) {
Data d = {
Data result;
return std::move(result);
}();
benchmark::DoNotOptimize(d);
}
}
BENCHMARK(BM_Move);
결과 (GCC 13, -O3):
BM_RVO 1000000 ns
BM_Move 1500000 ns (50% 느림)
정리
RVO vs NRVO
| 항목 | RVO | NRVO |
|---|---|---|
| 반환 대상 | 임시 객체 | 이름 있는 변수 |
| C++17 보장 | ✅ | ❌ (선택적) |
| 조건 | prvalue | 단일 변수 |
핵심 규칙
- return에 std::move 쓰지 마세요 (RVO 방해)
- 단일 변수 반환 (NRVO 활성화)
- C++17부터 RVO 보장
- 이동 생성자 구현 (NRVO 실패 대비)
체크리스트
- return에 std::move를 쓰지 않는가?
- 단일 변수를 반환하는가?
- 참조 반환이 아닌가?
- 이동 생성자가 구현되어 있는가?
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ 이동 시맨틱 | std::move 완벽 가이드
- C++ 복사 생략 | Copy Elision 가이드
- C++ rvalue 참조 | && 완벽 가이드
- C++ 이동 생성자 | Move Constructor
마치며
RVO는 함수 반환 시 복사를 생략하는 강력한 최적화입니다. 핵심 원칙:
- return에 std::move 쓰지 마세요
- 단일 변수 반환
- C++17부터 RVO 보장 return std::move(local)은 RVO를 방해하므로 절대 쓰지 마세요. 다음 단계: RVO를 이해했다면, C++ 이동 시맨틱 가이드에서 더 깊이 배워보세요.