28. 참조 축약을 숙지하라
보편 참조 생성자에서 인자로 보편 참조 매개변수를 초기화할 때, T (템플릿 매개변수) 에는 인자의 정보가 부호화 되어 있다.
(l-value/r-value 여부)
예시)
template<typename T>
void func(T&& param);
위 케이스에서 T 에는 param 에 전달된 정보가 존재한다.
예시)
Widget w; // l-value
Widget widgetFactory(); // r-value
func(w); // l-value 전달, T = Widget&
func(widgetFactory()); // r-value 전달, T = Widget
위 예시에서 각각 func 함수 인자로 l-value/r-value 가 전달된다.
l-value/r-value 에 따라 T에 연역되는 형식이 다르다.
C++ 에서는 참조에 대한 참조는 허용되지 않는다.
예시)
int x;
auto& &rx = x; // x를 참조하는 rx 를 참조
따라서 위 코드에서는 컴파일 에러가 발생한다.
하지만 보편 참조를 받는 함수 템플릿에 l-value 가 전달되는 경우, 참조에 대한 참조가 허용되는 것으로 보인다.
예시)
// template 인스턴스한 결과
void func(Widget& && param);
// 실제 결과
void func(Widget& param);
이 때 사용되는 것이 참조 축약 (reference collapsing) 이다.
특정 문맥에서는 참조에 대한 참조 산출이 허용된다.
그러한 문맥 중에는 템플릿 인스턴스화도 존재하며, 이때 참조 축약의 규칙이 적용된다.
참조 축약 규칙
만일 두 참조 중 하나라도 l-value 참조이면 결과는 l-value 참조이다.
그렇지 않다면 결과는 r-value 참조이다.
Widget& && param 의 경우, l-value 를 r-value 참조에 대입한 것이므로 규칙을 적용하면 l-value reference이다.
std::forward 의 동작도 참조 축약 덕분에 가능하다.
예시)
template<typename T>
void func(T&& param) {
...
someFunc(std::forward<T>(param));
}
template<typename T>
T&& forward(typename remove_reference<T>::type& param) {
return static_cast<T&&>(param);
}
func(w); // l-value 전달, T = Widget&
func(widgetFactory()); // r-value 전달, T = Widget
전달 인자에 따라 함수 내부에서의 로직은 다음과 같다.
l-value 전달
// 연역된 T 적용
template<typename T> Widget& && forward(typename remove_reference<Widget&>::type& param) { return static_cast<Widget& &&>(param); }
// reference collapsing 적용 및 type trait 적용
template<typename T> Widget& forward(Widget& param) { return static_cast<Widget&>(param); }
- func(w); 에서 T 가 Widget& 으로 연역됨
- forward의 T에도 Widget& 전달
- Return 타입과 캐스팅 타입에서 참조 축약 발생함
- 이미 Widget& 이므로 캐스팅 효과 없이 l-value refernce 전달
r-value 전달
// 연역된 T 적용
template<typename T> Widget&& forward(typename remove_reference<Widget>::type& param) { return static_cast<Widget&&>(param); }
// type trait 적용
template<typename T> Widget&& forward(Widget& param) { return static_cast<Widget&&>(param); }
- func(widgetFactory()); 에서 T 가 Widget 으로 연역됨
- forward의 T에도 Widget 전달
- 참조 축약 발생하지 않음
- Widget& (l-value reference)가 캐스팅되어 r-value refernce 전달
C++14 스타일 예시)
template<typename T>
T&& forward(typename remove_reference_t<T>& param) {
return static_cast<T&&>(param);
}
참조 축약이 일어나는 문맥
- 템플릿 인스턴스화
- auto 변수 형식 연역
- typedef 와 using
- decltype
auto 변수 형식 연역의 경우 템플릿 인스턴스와 거의 동일하다.
예시)
Widget w; // l-value
Widget widgetFactory(); // r-value
auto&& w1 = w; // l-value 전달, w1 = Widget&
// Widget& && w1
// Widget& w1
auto&& w2 = widgetFactory(); // r-value 전달, w2 = Widget
// Widget&& w2
w1 은 참조 축약으로 인해 l-value reference 가 된다.
w2 는 참조 축약 없이 r-value reference 가 된다.
보편 참조의 편리함은 여기에 있다.
개발자가 && 로 작성만 하면, 컴파일러에서 참조 축약 등 과정이 자동으로 이뤄진다.
typedef의 경우 연역 과정에서 참조 축약이 동작한다.
예시)
template<typename T>
class Widget {
public:
typedef T&& RvalueRefToT;
...
};
T가 l-value 인 int & 로 연역되는 경우, 참조 축약으로 인해 int& RvalueRefToT 가 된다.
이는 l-value reference 이므로, typedef 이름과 상이하다. (l-value/r-value ref 모두 될 수 있으므로 수정 필요)
decltype의 경우 컴파일러가 형식 분석할 때, 참조에 대한 참조가 발생하면 참조 축약이 이를 제거한다.