← 문제로

2. 이동 의미론과 복사 생략(RVO/NRVO)

난이도 중
내 리뷰 · C++
해설 · C++

해설 — 이동 의미론과 복사 생략(RVO/NRVO)

난이도: 중상

답변 프레임워크: 요약 → 핵심 개념 → 단계별 내부 동작 → 흔한 오해·함정 → 면접 포인트

요약

g++ -std=c++17 에서 예상 출력(흔한 NRVO 적용 가정):

ctor A
---
ctor B
---
ctor C
move C
dtor
===
dtor C
dtor B
dtor A
  • makeRVO(): prvalue 반환 → C++17 의 보장된 복사 생략. 복사도 이동도 없다. a 가 곧바로 그 자리에서 생성. 출력은 ctor A 하나.
  • makeNRVO(): 이름있는 지역 반환 → NRVO(선택적). g++ 는 적용한다. 출력 ctor B 하나.
  • makeMovedReturn(): return std::move(t) → NRVO 가 불가능해져 이동 생성자가 호출됨. ctor Cmove C(t.name 을 가져감) → 그 후 지역 t 소멸 시 name 이 빈 문자열이라 dtor(뒤에 공백) 출력. std::move 가 오히려 이동 1회 + 추가 소멸 출력을 만든다.

핵심 개념

  • 복사 생략(copy elision): 컴파일러가 임시/지역 객체의 복사·이동을 없애고 대상 자리에서 직접 생성하는 최적화.
    • 보장된 RVO (C++17): prvalue(이름 없는 임시)를 같은 타입 객체 초기화/반환에 쓰면 복사·이동이 반드시 제거된다(이동 생성자가 없어도 동작). Tracer a = makeRVO(); 그리고 return Tracer("A"); 가 여기 해당.
    • NRVO (Named RVO): 이름있는 지역을 반환할 때의 elision. 표준은 허용하지만 의무는 아니다. 대부분 컴파일러가 적용하나, 분기·다중 return 등으로 못 할 수 있다.
  • return 의 암묵적 이동: 지역을 반환하면 오버로드 결정에서 그 지역을 먼저 rvalue 로 취급한다(이동 생성자 우선). 그래서 NRVO 가 안 되더라도 보통 "이동"이지 "복사"가 아니다.

단계별 내부 동작

  1. Tracer a = makeRVO();
    • return Tracer("A") 는 prvalue. C++17 규칙으로 임시는 만들어지지 않고 a 자리에서 직접 생성 → ctor A 만. (생성자 안 name(std::move(n)) 은 std::string 의 이동이라 Tracer 의 출력엔 안 보임.)
  2. Tracer b = makeNRVO();
    • 지역 t("B") 생성(ctor B). return t; 에서 NRVO 로 t 가 곧 b → 추가 이동·복사 없음. (NRVO 미적용 가정 시엔 move B + 빈 dtor 가 더 보인다.)
  3. Tracer c = makeMovedReturn();
    • 지역 t("C") 생성(ctor C). return std::move(t)xvalue 반환이라 NRVO 대상이 아니다 → 반환 객체를 t 로부터 이동 생성(move C). 그 뒤 t 소멸: name 이 이동돼 비었으므로 dtor(공백). c 는 이동된 객체("C").
  4. main 종료: 역순 소멸 dtor C(c) → dtor B(b) → dtor A(a).

참고: "moved-from" 객체는 여전히 소멸된다. std::string 은 이동 후 유효하지만 미지정 상태 (대개 빈 문자열)라 dtor 의 name 이 빈 칸으로 보인다.

흔한 오해·함정

  • "return std::move(x) 가 빠르다"는 오해: 지역 반환은 어차피 이동(또는 elision)이다. std::move 를 붙이면 보장된 NRVO/elision 을 꺼서 강제로 이동을 만들고, elision 됐을 경우보다 느려진다(pessimizing move). 컴파일러 경고 -Wpessimizing-move 가 잡아준다.
  • "이동은 항상 일어난다"는 오해: C++17 prvalue 반환은 이동조차 없다(객체가 애초에 하나).
  • 이동 생성자에 noexcept: 컨테이너 재할당 시 이동을 쓰려면 noexcept 가 중요 (아니면 강한 예외 보장을 위해 복사로 폴백). 게임서버 핫패스 컨테이너에서 체감.
  • NRVO 비보장: 멀티 return/조건부 return 은 NRVO 가 깨질 수 있다. "한 변수만, 단일 경로로 반환"이 elision 친화적.

면접 포인트

  1. C++17 의 "보장된 복사 생략"과 NRVO(선택적)의 차이, 그리고 return std::move(local) 이 왜 안티패턴인지.
  2. return 시 지역이 rvalue 로 먼저 취급되는 규칙(이동 우선) — elision 이 안 돼도 복사가 아닌 이유.
  3. 이동 생성자 noexceptstd::vector 재할당/push_back 성능에 주는 영향.