← 문제로

50. std::move 의 정체와 const 객체의 "이동"

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

해설 — std::move 의 정체와 const 객체의 "이동"

난이도: 상

요약

std::move(x) 는 아무것도 옮기지 않는다. x 를 rvalue(정확히는 xvalue)로 캐스팅할 뿐이다. 실제 이동은 그 결과를 받는 생성자/대입 오버로드 선택에서 일어난다. const 객체에 std::move 를 걸면 결과 타입이 const W&& 가 되어 이동 생성자 W(W&&)(비-const 우변참조) 에 바인딩되지 못하고, 복사 생성자 W(const W&) 가 선택된다 — 즉 조용히 복사된다.

실행 결과(예측)

ctor a
ctor b
-- move non-const --
MOVE a
a.tag after move = []
-- move const --
COPY b
b.tag after move = [b]
-- done --

핵심 개념

  • std::move 는 캐스트다. 개념적으로 static_cast<remove_reference_t<T>&&>(x). 런타임 동작이 없고, 값 범주(value category)만 lvalue → xvalue 로 바꾼다.
  • 오버로드 해석이 이동/복사를 결정한다. W&& 파라미터는 우변값(및 xvalue)에 바인딩되고, const W& 는 거의 모든 것에 바인딩된다. 이동 생성자가 선택되어야 비로소 자원(여기서는 std::string 버퍼)이 옮겨진다.

단계별 내부 동작

  1. W c = std::move(a);
    • std::move(a) 의 타입은 W&&(비-const xvalue).
    • 오버로드 후보 W(W&&) 와 W(const W&) 중 W&& 가 더 정확히 일치 → 이동 생성자.
    • std::string 의 이동으로 a.tag 의 내부 버퍼가 c.tag 로 이전 → a.tag 는 "유효하지만 미규정(valid but unspecified)" 상태, 표준 라이브러리 구현상 보통 빈 문자열. 그래서 a.tag after move = [].
  2. W d = std::move(b); (b 는 const W)
    • std::move(b) 의 타입은 const W&&.
    • W(W&&) 파라미터는 const 를 버릴 수 없어 const W&& 를 받지 못한다.
    • 남은 후보 W(const W&) 가 const W&& → const W& 로 바인딩 가능 → 복사 생성자.
    • 복사이므로 b.tag 는 그대로 남아 b.tag after move = [b].

흔한 오해·함정

  • "std::move 를 쓰면 무조건 이동된다" — 아니다. const 객체, 이동 생성자가 없는 타입, const&& 를 못 받는 경우 등에서는 복사로 폴백된다(그리고 조용하다).
  • 멤버가 const 라 클래스 전체 이동이 복사로 떨어지는 경우도 같은 원리.
  • return std::move(local); 은 오히려 NRVO(복사 생략)를 방해할 수 있어 지역 반환에는 쓰지 않는 게 낫다(값 반환은 그대로 두면 컴파일러가 최적화).
  • moved-from 상태는 "유효하지만 미규정" — 재대입/clear() 후 재사용은 안전하나, 옮겨진 값이 특정 상태(예: 빈 문자열)라고 이식성 있게 가정하면 안 된다.

면접 포인트

  • std::move 는 런타임 없는 캐스트이며, 이동은 오버로드 선택의 결과다.
  • const + std::move = 복사(그리고 경고 없음) — 성능 함정으로 자주 등장.
  • moved-from 객체의 계약("유효하지만 미규정")과 안전한 재사용 규칙.