dev/C++

Effective Modern C++ #28 참조 축약을 숙지하라

dev_dev 2025. 6. 25. 21:47

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); }
  1. func(w); 에서 T 가 Widget& 으로 연역됨
  2. forward의 T에도 Widget& 전달
  3. Return 타입과 캐스팅 타입에서 참조 축약 발생함
  4. 이미 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); }
  1. func(widgetFactory()); 에서 T 가 Widget 으로 연역됨
  2. forward의 T에도 Widget 전달
  3. 참조 축약 발생하지 않음
  4. 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);
}

 

참조 축약이 일어나는 문맥

  1. 템플릿 인스턴스화
  2. auto 변수 형식 연역
  3. typedef 와 using
  4. 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의 경우 컴파일러가 형식 분석할 때, 참조에 대한 참조가 발생하면 참조 축약이 이를 제거한다.