55. 복사 생략(copy elision)과 RVO/NRVO
난이도 중내 리뷰 · C++
해설 · C++
해설 — 복사 생략(copy elision)과 RVO/NRVO
난이도: 중상
요약
반환 시 임시/지역 객체의 복사·이동을 없애는 최적화가 복사 생략이다. C++17부터 prvalue를
초기화에 쓰는 경우의 복사 생략은 "보장"(의무)된다 — Tracer x = MakePrvalue();는 복사/이동
생성자가 아예 호출되지 않고 객체가 x 자리에 직접 생성된다(복사/이동 생성자가 delete여도 OK).
반면 **NRVO(이름 있는 지역 반환)**는 C++17에서도 선택적이며, 컴파일러가 못 하면 이동(또는
복사)이 일어난다. 그리고 조건부 반환처럼 반환식이 "지역 이름(id-expression)"이 아니면, 반환 시
암시적 이동 규칙이 적용되지 않아 복사가 될 수 있다.
예측/실측 출력 (g++, -O0)
== prvalue ==
ctor
== nrvo ==
ctor
== cond ==
ctor
ctor
copy
dtor
dtor
== end ==
dtor
dtor
dtor
- prvalue:
MakePrvalue()의Tracer{}가x에 직접 생성 →ctor1회, copy/move 없음(보장된 생략). - nrvo:
MakeNRVO()의 지역t가y에 직접 생성 →ctor1회(NRVO 적용, move 없음). - cond:
Tracer a, c;→ctor2회.return b ? a : c;는 **삼항식(lvalue)**이라 "지역 이름을 반환"하는 경우가 아니다 → 반환 시 암시적 이동이 적용되지 않아 복사 생성(copy)으로z생성. 이어 함수 종료 시 지역c,a파괴(dtor2회, 역순). - end:
z,y,x파괴(역순) →dtor3회.
핵심 개념
- 보장된 복사 생략(C++17, prvalue):
T x = f();에서f()가 prvalue를 반환하면 임시가 존재하지 않고 결과가x에 직접 구체화(materialize). 이동/복사 생성자 접근성·존재와 무관. - NRVO(named RVO): 지역 객체를 그 이름으로 반환하면 컴파일러가 그 지역을 반환 슬롯에 직접 둘 수 있다. 의무가 아님 — 못 하면 이동(이동 가능하면), 그마저 없으면 복사.
- 반환 시 암시적 이동:
return local;에서local이 자동 저장기간 객체의 이름이면 오버로드 해석 시 rvalue로 취급되어 이동 생성자가 선택된다. 그러나return b?a:c;처럼 이름이 아니면 이 규칙이 적용되지 않아 복사가 된다(C++20/23에서 일부 완화 논의가 있으나 삼항은 여전히 주의).
단계별 내부 동작
- prvalue 반환: 호출자가 넘긴 반환 슬롯 주소에
Tracer{}를 in-place 생성. 임시 없음. - NRVO:
t를 반환 슬롯에 바인딩해 생성. 성공 시 이동조차 없음. - 조건부: 두 후보(a,c)가 있어 반환 슬롯에 미리 바인딩할 하나를 못 정함 → NRVO 불가. 게다가 반환식이 이름이 아니라 삼항이라 암시적 이동도 불가 → 복사.
흔한 오해·함정
- "
std::move를 붙이면 항상 빠르다" —return std::move(local);은 오히려 NRVO를 방해해 강제 이동으로 떨어뜨린다(prvalue면 보장 생략을 깨서 손해). 지역 반환엔std::move붙이지 말 것. - "C++17이면 NRVO도 보장" — 아니다. 보장되는 건 prvalue 경우뿐.
- 조건부/여러 반환 경로는 이동조차 안 되고 복사가 될 수 있다 — 삼항 대신
if로 각 분기에서return a;/return c;처럼 이름을 직접 반환하면 암시적 이동이 적용된다.
면접 포인트
- C++17 보장된 복사 생략(prvalue) vs 선택적 NRVO의 차이.
return std::move(x)가 왜 대개 안티패턴인가(생략/이동 방해).- 반환 시 암시적 이동이 "지역 이름"에만 적용된다는 규칙과 삼항 반환의 함정(복사 발생).