Item 28. 참조 축약(reference collapsing)을 숙지해라

C++은 reference의 reference를 허용하지 않는다.

하지만 타입을 추론할 때 컴파일러가 reference의 reference를 허용하는 상황이 있다.


참조 축약(reference collapsing)

참조 축약(reference collapsing)은 타입을 추론할 때 발생하는 reference의 reference를 특정한 규칙에 따라 축약하는 것을 말한다.

함수 템플릿에서 universal reference를 사용하는 경우, lvalue와 rvalue를 모두 인자로 받을 수 있다.

template <typename T>
void func(T&& param);

Widget widgetFactory(); // Widget을 반환하는 함수

Widget w; // Widget 타입의 변수

func(w); // func 함수를 lvalue로 호출, T는 Widget&로 추론됨

func(widgetFactory()); // func 함수를 rvalue로 호출, T는 Widget으로 추론됨

universal reference가 lvalue를 받는 경우 T의 타입은 Widget&로 추론된다.

그럼 템플릿 인스턴스화 결과는 다음과 같아야 한다.

void func(Widget& && param);

하지만 최종적으로 만들어지는 함수는 다음과 같다.

void func(Widget& param);

이는 컴파일러가 참조 축약(reference collapsing)을 통해 reference의 reference를 축약했기 때문이다.

reference collapsing 규칙은 다음과 같다.

T& & = T&
T& && = T&
T&& & = T&
T&& && = T&&

두 reference 중 하나라도 lvalue reference이면 lvalue reference로 축약된다.


std::forward 작동 원리

universal reference를 사용하는 함수 템플릿은 인자로 받은 lvalue, rvalue 타입을 그대로 반환하기 위해 std::forward를 사용한다.

template <typename T>
void f(T&& fParam) {
  someFunc(std::forward<T>(fParam));
}

이때 std::forward 는 reference collapsing rule을 이용해서 argument가 lvalue인지 rvalue인지 확인할 수 있다.

std::forward 내부는 다음과 같이 동작한다.

// namespace std 공간이라고 가정
template <typename T>
T&& forward(typename remove_reference<T>::type& param) {
  return static_cast<T&&>(param);
}

만약 f 함수에 Widget 타입의 lvalue가 들어오면 TWidget&로 추론된다.

Widget& && forward(typename remove_reference<Widget&>::type& param) {
  return static_cast<Widget& &&>(param);
}

//remove_reference는 reference-ness를 제거
Widget& && forward(Widget& param) {
  return static_cast<Widget& &&>(param);
}

여기서 reference collapsing rule을 적용하면 다음과 같다

Widget& forward(Widget& param) {
  return static_cast<Widget&>(param);
}

즉, std::forward는 lvalue reference를 받아 lvalue로 돌려주었다.

만약 f 함수에 Widget 타입의 rvalue가 들어오면 TWidget으로 추론된다.

Widget&& forward(Widget& param) {
  return static_cast<Widget&&>(param);
}

이 상황에서는 reference의 reference는 없으므로 reference collapsing rule을 적용할 필요가 없다

즉, std::forward는 rvalue reference를 받아 rvalue로 돌려주었다.

C++14부터는 std::remove_reference_t 를 사용해서 std::forward를 더 간결하게 표현할 수 있다.

// namespace std 공간이라고 가정
template <typename T>
T&& forward(remove_reference_t<T>& param) {
  return static_cast<T&&>(param);
}


reference collapsing이 발생하는 상황

reference collapsing이 발생하는 상황은 네 가지이다.

템플릿 인스턴스화(template instantiation)

앞서 예시처럼 템플릿 인스턴스화 과정에서 reference의 reference가 발생할 수 있고 reference collapsing rule에 따라 축약한다.

auto 타입 추론

auto도 template과 같은 reference collapsing이 발생한다.

/* universal reference auto에 lvalue를 초기화하는 경우 */

Widget w; // Widget 타입의 변수 (lvalue)
auto&& w1 = w; // auto는 Widget&로 추론 (universal reference에 lvalue가 들어온 경우)

// reference의 reference 발생
Widget& && w1 = w; 
// reference collapsing
Widget& w1 = w;
/* universal reference auto에 rvalue를 초기화하는 경우 */

Widget widgetFactory(); // Widget을 반환하는 함수
auto&& w2 = widgetFactory(); // auto는 Widget으로 추론

// reference collapsing이 발생하지 않음
Widget&& w2 = w;

typedef 혹은 alias declaration 지정 및 사용

typedef 혹은 alias declaration(using)을 사용할 때도 reference collapsing이 발생할 수 있다.

template <typename T>
class Widget {
public:
  typedef T&& RvalueRefToT;
};

// Widget의 lvalue reference 타입으로 인스턴스화
Widget<int&> w;

// reference의 reference 발생
typedef int& && RvalueRefToT;
// reference collapsing
typedef int& RvalueRefToT

Widget을 lvalue reference 타입으로 인스턴스화 하면 RvalueRefToT라는 이름에 무색하게 lvalue reference에 대한 typedef가 됨

decltype 지정 및 사용

decltype과 reference를 함께 쓰면 reference collapsing이 발생할 수 있다.

int& f1();  // int& 타입을 리턴하는 함수
int&& f2(); // int&& 타입을 리턴하는 함수

int a = 10;

decltype(f1())& b = a;
// reference의 reference 발생
int& & b = a;
// reference collapsing
int& b = a;

decltype(f2())&& x = 10;
// reference의 reference 발생
int&& && x = 10;
// reference collapsing
int&& x = 10;


카테고리:

업데이트: