← 문제로

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에 직접 생성 → ctor 1회, copy/move 없음(보장된 생략).
  • nrvo: MakeNRVO()의 지역 t가 y에 직접 생성 → ctor 1회(NRVO 적용, move 없음).
  • cond: Tracer a, c; → ctor 2회. return b ? a : c;는 **삼항식(lvalue)**이라 "지역 이름을 반환"하는 경우가 아니다 → 반환 시 암시적 이동이 적용되지 않아 복사 생성(copy)으로 z 생성. 이어 함수 종료 시 지역 c, a 파괴(dtor 2회, 역순).
  • end: z,y,x 파괴(역순) → dtor 3회.

핵심 개념

  • 보장된 복사 생략(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)가 왜 대개 안티패턴인가(생략/이동 방해).
  • 반환 시 암시적 이동이 "지역 이름"에만 적용된다는 규칙과 삼항 반환의 함정(복사 발생).