본문 바로가기

진리는어디에/C++

[C++] 레퍼런스 콜랩싱(reference collapsing)

들어가며

이전 포스트 [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에 대해서는 다음 포스트에서 계속 이어 다루도록 하겠다.

부록 1. 같이 읽으면 좋은 글

유익한 글이었다면 공감(❤) 버튼 꾹!! 추가 문의 사항은 댓글로!!