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 a→T = int&, 매개변수 타입int& &&→ 참조 축약 →int&. - const lvalue
const int c→T = const int&→const int&. - rvalue
42→T = 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 = int→static_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)...); }처럼 팩으로 확장해야 각 인자의 범주가 개별 보존된다.
면접 포인트
T&&(추론 문맥)는 전달 참조이며, 참조 축약으로 lvalue 인자 → lvalue 참조, rvalue 인자 → rvalue 참조가 된다.std::forward<T>는static_cast<T&&>로, 추론된T에 인코딩된 원래 값 범주를 복원한다 — 래퍼에서 이걸 빼면 이동이 복사로 퇴화한다.- "타입이 rvalue 참조"여도 이름 붙은 변수는 lvalue 다. 그래서
relayPlain(42)가lvalue를 출력한다 — 완벽한 전달의 본질적 이유.