Effective C++/Effective Modern C++
[Effective Modern C++] 28. 참조 축약(reference collapsing)
김디트
2025. 3. 21. 10:55
728x90
항목 28. 참조 축약을 숙지하라
보편 참조의 타입 추론
template<typename T>
void func(T&& param);
Widget widgetFactory(); // rValue를 리턴하는 함수
Widget w; // 변수(lValue)
func(w); // T는 Widget&로 추론된다.
func(widgetFactory()); // T는 Widget로 추론된다.
- lValue인지 rValue인지에 따라 참조가 타입에 포함되는지 여부가 갈린다.
- lValue : 참조(&)가 타입에 포함되어 추론된다.
- rValue : 비참조로 추론된다.
타입 추론과 참조 축약
- 참조에 대한 참조는 위법 사항이다.
int x;
...
auto& & rx = x; // 에러!!!
- 그렇다면 참조를 받는 함수 템플릿에 lValue를 넘겨준 상황(위 예제에서의 func(w);)은 어떻게 진행될까?
// 다음처럼 추론될 것이다.
void func(Widget& && param);
// 하지만 컴파일 에러는 발생하지 않는다.
- 보편 참조 param은 lValue로 처리되므로, param의 타입은 lValue 참조가 된다.
- 즉, 최종적인 타입 추론은 아래와 같다.
void func(Widget& param);
- 이것이 바로 참조 축약(reference collapsing)이다.
- 참조에 대한 참조는 위법이지만,
- 특정 상황에서는 컴파일러가 참조에 대한 참조를 산출하는 것이 허용된다.
- 참조 축약이 발생하는 네 가지 상황은 가장 아래에 정리되어 있다.
참조 축약의 규칙
- 참조는 두 종류(lValue, rValue)이므로 참조에 대한 참조가 가능한 조합은 총 네 가지이다.
- lValue + lValue
- lValue + rValue
- rValue + lValue
- rValue + rValue
- 이 조합들은 다음 규칙에 따라 하나의 참조로 축약된다.
- 만일 두 참조 중 하나라도 lValue 참조라면
- 결과는 lValue 참조이다.
- 둘 다 rValue 참조라면
- 결과는 rValue 참조이다.
- 만일 두 참조 중 하나라도 lValue 참조라면
std::forward와 참조 축약
- std::forward가 작동하는 것은 이 참조 축약 덕분이다.(항목 25 참조)
template<typename T>
void f(T&& fParam)
{
...
someFunc(std::foward<T>(fParam));
}
- fParam은 보편 참조이므로 f에 전달된 매개변수의 타입이 lValue인지 rValue인지에 따라 다르게 부호화한다.
- 여기서 std::foward는 fParam이 rValue로 넘어왔을 때만 rValue로 캐스팅한다.
- 이것이 가능한 이유를 아래 forward의 구현을 보면서 알아보자.
template<typename T>
T&& forward(typename remove_reference<T>::type& param)
{
return static_cast<T&&>(param);
}
// C++14 버전
template<typename T>
T&& forward(remove_reference_t<T>& param)
{
return static_cast<T&&>(param);
}
- f에 전달된 인수가 lValue인 Widget&이라면
// f에 전달된 인수가 lValue인 Widget&이라면
Widget& && forward(typename remove_reference<Widget&>::type& param)
{
return static_cast<Widget& &&>(param);
}
// remove_reference를 반영
Widget& && forward(Widget& param)
{
return static_cast<Widget& &&>(param);
}
// 참조 축약 반영
Widget& forward(Widget& param)
{
return static_cast<Widget&>(param); // lValue 참조가 리턴된다.
}
- f에 전달된 인수가 rValue인 Widget이라면
// f에 전달된 인수가 rValue인 Widget이라면
Widget&& forward(typename remove_reference<Widget>::type& param)
{
return static_cast<Widget&&>(param);
}
// remove_reference를 반영
// 참조에 대한 참조가 없으므로 참조 축약은 없다.
Widget&& forward(Widget& param)
{
return static_cast<Widget&&>(param); // rValue가 리턴된다.
}
참조 축약이 발생하는 상황
- 템플릿 인스턴스화
- 위에서 다루었다.
- auto 변수에 대한 타입 추론
- 본질적으로 템플릿 타입 추론과 같다.(항목 2 참조)
-
Widget widgetFactory(); // rValue 리턴 함수 Widget w; // 변수(lValue) auto&& w1 = w; // lValue 참조를 넘긴다. // 추론 전개 양상 Widget& && w1 = w; // 참조 축약 발생 Widget& w1 = w; auto&& w2 = widgetFactory(); // rValue를 넘긴다. // 추론 전개 양상 // 참조 축약은 없다. Widget&& w2 = widgetFactory();
- typedef와 별칭 선언(항목 9 참조)의 지정 및 사용
-
template<typename T> class Widget { public: typedef T&& RvalueRefToT; ... }; Widget<int&> w; // 위의 경우 typedef는 참조 축약이 발생한다. typedef int& && RvalueRefToT; // 참조 축약 발생 typedef int& RvalueRefToT;
- 이 경우 typedef를 사용해서, 오히려 혼란을 야기하는 코드가 되었다.
- lValue 참조로 인스턴스화하면 lValue 참조에 대한 typedef가 되지만,
- 형태 상으로는 rValue가 되어야 할 것처럼 생겼다.
-
- decltype 사용(항목 3 참조)
보편 참조는 새로운 종류의 참조가 아니다.
- 아래 조건이 맞을 때의 rValue 참조이다.
- 타입 추론 시 lValue와 rValue가 구분되어 추론된다.
- lValue면 T&, rValue면 T로
- 참조 축약이 적용된다.
- 타입 추론 시 lValue와 rValue가 구분되어 추론된다.
- 하지만 참조 축약 문맥을 고려하지 않고 직관적으로 받아들일 수 있으므로 보편 참조란 개념은 유용하다.
728x90