template<typename T>
void func(T&& param);
Widget widgetFactory(); // 오른값을 돌려주는 함수
Widget w; // 변수 (왼값)
func(w); // func를 왼값 호출, T는 Widget&로 연역
func(widgetFactory()); // func를 오른값 호출, T는 Widget으로 연역
두 func 모두 Widget이 전달되지만 Widget이 왼값인지 오른값인지에 따라 템플릿 매개변수 T에 대해 연역되는 형식이 다르다. 보편참조가 오른값/왼값 중 어느 값 참조가 될 것인지 결정한다. 그리고 이 메커니즘은 std::forward의 기본이 된다.
우선 C++에서 참조에 대한 참조는 위법이다. 하지만 보편 참조를 받는 함수 템플릿에 왼값을 넘겨주는 것은 컴파일이 된다.
int x;
auto& & rx = x; // 참조에 대한 참조는 선언 불가
template<typename T>
void func(T&& param);
func(w); // func를 왼값으로 호출, T는 Widget&로 연역
T는 Widget&로 연역되고 그 형식으로 템플릿을 인스턴스화 하면
void func(Widget& && param);
이 된다. 보편참조 param은 왼값으로 초기화 되므로 param의 형식은 왼값 참조가 된다. 하지만 실제로 만들어지는 최종적인 함수의 서명은
void func(Widget& param);
이다. 참조에 대한 참조는 위법이지만 템플릿 인스턴스화 같은 특정 문맥에서는 참조 에 대한 참조를 산출하는 것이 허용된다. 이를 참조 축약이라고 하며, 다음의 규칙이 적용된다. '만일 두 참조중 하나라도 왼값 참조이면 결과는 왼값참조이다. 그렇지 않으면 결과는 오른값 참조이다.'
기본적으로 std::forward가 작동하는 것은 참조축약 덕분이며, 보편참조 매개변수에 적용한다.
tempalte<typename T>
void f(T&& tParam)
{
...
someFunc(std::forward<T>(fParam)); // fParam을 someFunc로 전달
}
fParam은 보편참조이므로 f에 전달된 인수가 왼값인지 오른값인지에 대한 정보가 형식 매개변수 T에 부호화 된다. std::forward는 만일 f에 전달되는 인수가 오른값이라는 점이 T에 부호화 되어 있으면 (T가 비참조형식이면) 그럴 때에만 fParam(왼값)을 오른값으로 캐스팅하는 것이다.
다음은 std::forward의 작동 방식을 간단히 구현한 것이다.
using namespace std;
template<typename T>
T&& forward(typename remove_reference<T>::type& param)
{
return static_cast<T&&>(param);
}
우선 첫번째로, f에 전달된 인수가 Widget 형식의 왼값일 때 T는 Widget&로 연역되며, std::forward 호출은 std::forward<Widget&>형태로 인스턴스화 된다.
Widget& && forward(typename remove_reference<Widget&>::type& param)
{ return static_cast<Widget& &&>(param); }
형식특질 remove_reference<Widget&>::type은 Widget을 산출하며, 참조축약은 반환 형식과 캐스팅에도 적용되므로 정리하면
Widget& forward(Widget& param)
{ return static_cast<Widget&>(param); }
이 예에서 보듯 함수 템플릿 f에 왼값 인수가 전달되면 std::forward는 왼값 참조를 받아 왼값 참조를 돌려주는 형태로 인스턴스화 된다. param의 형식이 이미 Widget&이므로 std::forward내부의 캐스팅은 효과가 없다.
두번째로 f에 전달된 인수가 Widget 형식의 오른값일 때 T는 Widget으로 연역되며, f 내부의 std::forward 호출은 std::forward<Widget>으로 인스턴스화 된다.
Widget&& forward(typename remove_reference<Widget>::type& param)
{ return static_cast<Widget&&>(param); }
std::remove_reference를 비참조 형식 Widget에 적용함녀 애초의 형식인 Widget이 산출되므로
Widget&& forward(Widget& param)
{ return static_cast<Widget&&>(param); }
여기에는 참조에 대한 참조가 없기 때문에 참조 축약도 없다.
참조 축약이 일어나는 문맥은 4가지이다. 첫번째로, 템플릿의 인스턴스화이다. 두번째는 auto 변수에 대한 형식 연역이다. 이 경우 세부 사항은 템플릿 인스턴스화의 경우와 본질적으로 같은데, auto변수의 형식 연역 은 템플릿 형식 연역과 본질적으로 같기 때문이다.
template<typename T>
void func(T&& param);
Widget widgetFactory();
Widget w;
func(w); // func를 왼값으로 호출, T는 Widget&
func(widgetFactory()); // func를 오른값으로 호출, T는 Widget
auto&& w1 = w; // w1을 왼값으로 초기화. auto는 Widget&로 연역
// 결국
Widget& w1 = w; // 와 동일하다
auto&& w2 = widgetFactory(); // w2를 오른값으로 초기화, auto는 Widget으로 연역
// 결국
Widget&& w2 = widgetFactory(); // 와 동일하다
결과적으로 형식 연역에서 형식 연역에서 왼값과 오른값이 구분되고, 참조 축약이 적용되는 문맥에서 보편참조는 사실상 오른쪽 참조이다.
세번째는 typedef와 별칭선언의 지정 및 사용이다. typedef가 지정 또는 평가되는 도중에 참조에 대한 참조가 발생하면 참조축약이 참조에 대한 참조를 제거한다.
template<typename T>
class Widget {
public:
typedef T&& RvalueRefToT;
...
{;
Widget<int&> w;
// Widget의 T를 int&로 대체하면
typedef int& && RvalueRefToT;
// 참조 축약에 의해
typedef int& RvalueRefToT;
Widget을 왼값 참조 형식으로 인스턴스화 하면 RvalueRefToT는 왼값 참조에 대한 typedef가 된다.
마지막으로는 decltype사용이다. 컴파일러가 decltype에 관여하는 형식을 분석하는 도중 참조에 대한 참조가 발생하면 참조 축약이 그것을 제거한다.
Conclusion
- 참조 축약은 템플릿 인스턴스화, auto 형식 연역, typedef와 별칭 선언의 지정 및 사용, decltype의 지정 및 사용이라는 네 가지 문맥에서 일어난다.
- 컴파일러가 참조 축약 문맥에서 참조에 대한 참조를 만들어 내면 그 결과는 하나의 참조가 된다. 원래 두 참조중 하나라도 왼값 참조이면 결과는 왼값 참조이고 아니면 오른값 참조이다.
- 형식 연역이 왼값과 오른값을 구분하는 문맥과 참조 축약이 일어나는 문맥에서 보편 참조는 오른값 참조이다.
Reference
Effective Modern C++ 항목 28 : 참조축약을 숙지하라
'C++ > Effective Modern C++' 카테고리의 다른 글
완벽 전달이 실패하는 경우들 1 (0) | 2020.07.29 |
---|---|
C++11에서 이동 의미론이 항상 도움이 될까 (0) | 2020.07.28 |
보편참조에 대한 중복적재 대신 사용할 수 있는 기법들을 알아두라 2 (0) | 2020.07.23 |
보편참조에 대한 중복적재 대신 사용할수 있는 기법들 1 (0) | 2020.07.22 |
보편 참조에 대한 중복적재를 피하라 (0) | 2020.07.21 |
댓글