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 참조이다.

 

 

 

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로
    • 참조 축약이 적용된다.
  • 하지만 참조 축약 문맥을 고려하지 않고 직관적으로 받아들일 수 있으므로 보편 참조란 개념은 유용하다.
728x90