들어가며
이전 포스트 [C++] r-value 레퍼런스(reference) 완벽 가이드를 통해서 r-value 레퍼런스가 무엇인지, l-value는 l-value 레퍼런스로만 참조 될 수 있고, r-value는 r-value 레퍼런스로만 참조 될 수 있다는 것을 살펴 보았다.
이번 포스트에서는 좀 더 깊이 들어가 참조를 참조하는 타입은 C++에서 어떻게 처리되고 살펴 보도록하자. 레퍼런스 콜랩싱이라는 이름이 생소해서 그렇지 아주 간단한 내용이니까 가벼운 마음으로 읽고 넘어 가도록 한다.
참조의 참조 타입
아래의 코드 7라인은 int& 타입에 &를 추가하여(&& 처럼 붙어 있지 않고 떨어져 있다는 것에 주목) 참조를 참조하는 타입을 만들고 있다. 괴랄하게 생긴만큼 문법적으로도 맞지 않으며 컴파일 하게 되면 참조의 참조 타입은 만들 수 없다는 에러를 출력한다.
int main()
{
int n = 0;
int& lr = n; // l-value reference
int&& rr = 0; // r-value reference
int& & ref2ref = lr; // error. 레퍼런스의 레퍼런스 타입
}
C++은 포인터의 포인터 타입은 지원하지만 레퍼런스의 레퍼런스 타입은 문법적으로 지원하지 않는다. 즉, 직접 코드를 사용해서 참조를 가리키는 참조 타입을 만들 수는 없다는 말이다.
하지만 아래 코드 처럼 decltype 과 같은 연산자를 이용한 타입 추론(type deduction)을 통해서 참조 타입을 추론하고 그 타입을 가리키는 참조 타입을 만드는 경우에는 참조를 참조하는 타입을 만들 수 없다는 에러는 발생 시키지 않는다.
int main()
{
int n = 0;
int& lr = n; // l-value reference
int&& rr = 0; // r-value reference
decltype(lr)& r1 = n; // int& &
decltype(lr)&& r2 = 0; // int& &&
decltype(rr)& r3 = n; // int&& &
decltype(rr)&& r4 = 0; // int&& &&
}
위 코드의 7라인 부터 살펴 보면 l-value 레퍼런스인 lr을 decltype으로 추론해 내고 뒤에 &를 붙여 int& &과 같은 타입을 만들어 냈다. 그와 비슷하게 아래의 코드들도 각각 다음과 같은 결과를 갖게 된다.
- decltype(lr)& r1 -> int& &
- decltype(lr)&& r2 -> int& &&
- decltype(rr)& r3 -> int&& &
- decltype(rr)&& r3 -> int&& &&
이렇게 타입 추론을 통해 참조를 가리키는 참조 타입이 발생하게 되면 C++은 "reference collapsing"이라는 규칙에 따라 최종 타입을 결정한다.
Reference collapsing 규칙
참조의 참조 타입 | 결과 |
Type& & | Type& |
Type& && | Type& |
Type&& & | Type& |
Type&& && | Type&& |
C++은 참조를 참조하는 타입이 발생하면 위의 규칙에 따라 최종 타입을 결정한다. 외우기 쉽다. 마지막 r-value 레퍼런스(&&)를 참조하는 r-value 레퍼런스의 결과만 r-value 레퍼런스고 나머지는 l-value 레퍼런스(&)다.
마치 열성 인자, 우성인자 처럼 l-vlaue 참조가 하나라도 있으면 l-value 참조로 콜랩싱 되고, 둘 다 r-value 일 경우에만 r-value 참조로 콜랩싱된다.
decltype(lr)& r1 = n; // int& & -> int&, ok
decltype(lr)&& r2 = 0; // int& && -> int&, error. l-value 참조에 r-value
decltype(rr)& r3 = n; // int&& & -> int&, ok
decltype(rr)&& r4 = 0; // int&& && -> int&&, ok
간단하지만 많이 사용되는 아주 중요한 규칙이니까 꼭 알아 두도록하자.
Reference Collapsing이 적용 되는 경우
- typedef
- using
- decltype
- template
참조를 참조하는 타입은 직접 코드를 사용해서는 만들 수 없지만 위 네 가지의 경우는 가능하며, 참조의 참조가 발생하는 경우 앞에서 말한 레퍼런스 콜랩싱 규칙이 적용되어 최종 타입으로 변경 된다.
예제를 살펴 보면서 어떻게 레퍼런스를 참조하는 타입을 만들 수 있는지 살펴 보자.
template<typename T> void foo(T&& arg)
{
}
int main()
{
int n = 0;
typedef int& LREF;
LREF&& r1 = n; // int& && -> int&
using RREF = int&&;
RREF&& r2 = 0; // int&& && -> int&&
decltype(r2)&& r3 = 0; // int&& && -> int&&
foo<int&>(n); // foo(int& && arg) -> foo(int& arg)
}
9~10라인은 int&을 LREF로 typedef를 걸어 놓고 r-value 레퍼런스를 뒤에 달아 주었다. 이런 경우 우성인 l-value 레퍼런스로 콜랩싱 된다. 나머지들도 &과 &&이 섞여 있는 경우 &로 최종 타입이 결정된다. r-value 레퍼런스(&&)가 되는 경우는 int&& 타입을 &&로 참조할 때 뿐이다
마치며
앞에서 다룬 네 가지 참조의 참조 타입 중 특히 템플릿 인자에 대한 T&&는 forwarding reference라는 아주 중요한 개념을 담고 있다. forwarding reference에 대해서는 다음 포스트에서 계속 이어 다루도록 하겠다.