← 문제로

8. 완벽한 전달(perfect forwarding)과 보편 참조, 참조 축약

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

해설 — 완벽한 전달(perfect forwarding)과 보편 참조, 참조 축약

난이도: 중상

요약

템플릿 매개변수 T&& 는 "rvalue 참조"가 아니라 전달 참조(forwarding reference, 일명 보편 참조) 다. 인자가 lvalue 면 T 가 참조 타입으로 추론되고 참조 축약으로 매개변수 타입이 lvalue 참조가, rvalue 면 T 가 비참조로 추론되어 rvalue 참조가 된다. std::forward<T>(x) 는 이 T 를 이용해 원래 값 범주를 복원한다. 핵심 함정: 이름 붙은 매개변수 x 는 그 자체로 lvalue 라, forward 없이 그냥 넘기면 rvalue 성을 잃는다.

실행 결과

lvalue          // (1) relayForward(a)
const-lvalue    // (2) relayForward(c)
rvalue          // (3) relayForward(42)
lvalue          // (4) relayPlain(a)
lvalue          // (5) relayPlain(42)   ← forward 없으면 rvalue 가 lvalue 로

핵심 개념

1. 전달 참조의 추론과 참조 축약

template<class T> void f(T&& x) 에서 인자별로:

  • lvalue int aT = int&, 매개변수 타입 int& && → 참조 축약 → int&.
  • const lvalue const int cT = const int&const int&.
  • rvalue 42T = int(비참조) → int&&.

참조 축약 규칙: & &, & &&, && & 는 모두 & 로, && &&&& 로 축약된다. "lvalue 참조가 하나라도 끼면 lvalue 참조".

2. std::forward 의 역할

std::forward<T>(x)static_cast<T&&>(x) 와 같다. 위 추론된 T 를 그대로 끼우면:

  • T = int&static_cast<int& &&> = static_cast<int&>lvalue 로 전달target(int&).
  • T = intstatic_cast<int&&>rvalue 로 전달target(int&&). 즉 forward 는 "들어온 값 범주를 추론된 T 에 인코딩해 두었다가 그대로 복원"한다. (1)(2)(3)이 호출 인자의 범주를 정확히 보존하는 이유다.

3. 이름 붙은 매개변수는 lvalue (relayPlain 함정)

relayPlain(42) 에서 T = int, x 의 타입은 int&& 다. 그러나 "타입이 rvalue 참조"인 것과 "표현식이 rvalue"인 것은 다르다. 이름이 있는 x 는 표현식으로서 lvalue 이므로, target(x)target(int&) 를 고른다 → (5)가 lvalue. rvalue 성을 유지하려면 target(std::forward<T>(x))(또는 std::move) 가 필요하다. 이것이 (3)과 (5)가 갈리는 지점이다.

흔한 오해·함정

  • T&& = rvalue 참조라고 단정: 템플릿 추론 문맥의 T&& 만 전달 참조다. void g(int&&)(구체 타입), vector<T>::push_back(T&&)(클래스 T 는 이미 고정) 은 전달 참조가 아닌 그냥 rvalue 참조다. auto&& 도 전달 참조.
  • std::forward 를 move 처럼 무조건 캐스팅: forward 는 T 에 의존해 조건부로 캐스팅한다. lvalue 로 들어온 걸 forward 하면 lvalue 로 남는다(원치 않는 무조건 move 와 다름).
  • forward 누락: 래퍼/팩토리(emplace, make_unique 류)에서 forward 를 빼먹으면 항상 복사가 일어나 이동 최적화가 사라진다. "완벽한 전달"이 깨지는 가장 흔한 버그.
  • 다중 인자: template<class... A> void f(A&&... a){ g(std::forward<A>(a)...); } 처럼 팩으로 확장해야 각 인자의 범주가 개별 보존된다.

면접 포인트

  1. T&&(추론 문맥)는 전달 참조이며, 참조 축약으로 lvalue 인자 → lvalue 참조, rvalue 인자 → rvalue 참조가 된다.
  2. std::forward<T>static_cast<T&&> 로, 추론된 T 에 인코딩된 원래 값 범주를 복원한다 — 래퍼에서 이걸 빼면 이동이 복사로 퇴화한다.
  3. "타입이 rvalue 참조"여도 이름 붙은 변수는 lvalue 다. 그래서 relayPlain(42)lvalue 를 출력한다 — 완벽한 전달의 본질적 이유.