이전 정리글은
2022.01.07 - [프로그래밍/C/C++] - C++ rvalue reference 우측값 참조 속성
이번 정리 시간은 C++ 에서의 Universal reference 와 Reference Collapsing Rules 에 대해 정리해보자.
Universal reference
유니버설 레퍼런스(universal reference)는 우측값 참조(rvalue reference)가 될 수도 있고 좌측값 참조(lvalue reference)도 될 수 있는 것을 말한다. 그럼 universal reference 는 어떻게 표현될까? 표현식 자체는 우측값 참조(rvalue reference)와 동일한 '&&' 이다. 다른점은 컴파일러에 의해 타입 추론이 발생할 경우 universal reference 가 된다. 해당 경우가 되는 상황을 두 가지정도 알아보자.
&& in template argument
템플릿에서 인자 타입에 '&&' 표현식이 쓰면 universal reference 이다.
template <typename T>
void func(T&& param){
// do something.
}
int main()
{
int i = 0;
func(i);
func(0);
return 0;
}
위 코드를 보면 func 을 좌측값(lvalue) 과 우측값(rvalue)을 통해서 모두 호출하는데 성공하고 있다. 좌측값을 넘길 경우에는 T 는 T& 타입으로 추론되며 우측값을 넘길 경우에는 T는 T로 추론된다. 그러므로 위 함수를 풀어쓰면 아래와 같다.
void func(int& param){
// do something.
}
void func(int&& param){
// do something.
}
위에서 정리했듯이 타입 추론이 발생해야만 universal reference 이다. 아래 같은 경우는 rvalue reference 이다.
template <typename T>
void func(std::queue<T>&& param);
templae <typename T>
void func2(const T&& param);
위 코드에서 func의 T는 큐 안에 원소에 대한 타입이지 param 의 타입은 큐인 거이다. func2 에서 T 는 universal reference 같지만 앞에 const 가 붙었기 때문에 우측값 참조(rvalue reference)이다.
또 한가지 중요한 점은 타입 추론이란 것은 수행 시점에 일어나기에 항상 이점을 유념해야 한다. 아래 코드에서 push_back 함수에서의 T&& 는 우측값 참조(rvalue reference)이다. 왜냐하면 push_back 함수가 호출되는 시점에 T는 이미 타입이 정해져 있기 때문이다.
template <typename T, type Allocator = allocator<T>>
class vector
{
void push_back(T&& x);
};
다음은 rvalueness 와 lvalueness 에 대해서 정리해 보자. 아래 코드를 보자.
template<typename T>
class clsObject
{
...
template <typename Y>
clsObject(Y&& rhs);
...
};
Y는 타입 추론이 발생하기 때문에 Universal reference 이다. 그럼 외부에서 호출 시 rvalue 또는 lvalue를 넘겨 받을 수 있으며 넘겨받으면 rvalue 일 경우에는 rvalueness 를 유지해야 하며 lvalue일 경우에는 lvalueness 를 유지해야 한다. 이러한 역할을 하는 함수가 std::foward() 이다. clsObject 의 rhs 자체는 생성자 내부에서 lvalue 이기에 들어온 원래 값이 rvalue 인지 lvalue 인지 바운드 시키기 위해서 foward() 함수를 사용할 수 있다. 아래 코드도 보도록 해보자.
template<typename T>
class clsObject
{
...
clsObject(clsObject&& rhs);
...
};
위 코드에서 우측값 참조를 받는 생성자의 rhs 자체는 lvalue 이기에 내부에서 rvalueness를 바운드시키기 위하여 std::move() 함수를 쓸 수 있다. 이전 글에서 정리했듯이 move는 좌측값(lvalue)를 우측값 참조(rvalue reference)로 타입 캐스팅해준다.
&& in auto
auto 타입 뒤에 쓰이는 '&&' 표현식도 타입 추론이 필요하기에 universal reference 이다.
clsObject&& obj1 = clsObject();
auto&& obj2 = obj1;
위 코드를 확인하면 obj2 는 좌측값 참조(lvalue reference)로 추론되다. 이유는 다들 보이겠지만 obj1 자체는 변수이기에 좌측값(lvalue) 이기 때문이다.
Reference Collapsing Rules
Reference Collapsing Rules 은 무엇일까? 타입 추론 발생 시 우측값 참조(rvalue reference) 또는 좌측값 참조(lvalue reference) 인지 결정하는 법칙이라고 생각하면 된다. 앞에서 universal refererence 타입 T&& 의 타입 추론 시 좌측값(lvalue)이 들어오면 T는 T& 로 추론되고 우측값(rvalue)이 들어오면 T로 추론된다고 했다. 예를 들어 int 타입의 값이 들어온다고 했을 경우 다음과 같이 표현될 수 있다.
template typename<T>
void func(T&& param)
{
}
int main()
{
int x = 2;
func(x);
// lvalue이기 때문에 func(int& && param);
func(2);
// rvalue이기 때문에 func(int&& param);
return 0;
}
& && 이런 표현식은 존재하지 않는다. 그래서 이런 부분을 축약하는 규칙이 존재하는데 그것이 Reference Collapsing Rules 인 것이다.
규칙은 다음과 같다.
& (lvalue reference) + && (rvalue reference) = & && (rvalue reference) + & (lvalue reference) = & & (lvalue reference) + & (lvalue reference) = & && (rvalue reference) + && (rvalue reference) = && |
좌측값 참조(lvalue reference)가 한개라도 섞이면 좌측값 참조가 된다라고 생각하면 헷갈리지 않을 것이다.
위의 Reference Collapsing Rules 이 적용되는 영역 한 부분을 더보자. 그것은 typedef 이며 다음 코드를 확인하자.
template <typename T>
class clsObject
{
typedef T& refType;
};
clsObject<int &> obj;
위 코드에서 obj 는 T 로 int& 를 넘기고 있다. 그러면 refType 의 T& 는 int& & 이 되고 이것은 앞선 룰에 따라 int& 이 된다.
다음 정리글은
2022.01.08 - [프로그래밍/C/C++] - C++ Perfect forwarding (std::forward 의 역할)
'프로그래밍 > C/C++' 카테고리의 다른 글
C++ template 과 type 추론 (0) | 2022.01.11 |
---|---|
C++ Perfect forwarding (std::forward 의 역할) (0) | 2022.01.08 |
C++ rvalue reference 우측값 참조 속성 (0) | 2022.01.07 |
C++ rvalue reference (우측값 참조) move semantics (0) | 2022.01.06 |
C++ Lvalue (좌측값) Rvalue (우측값) Rvalue reference (우측값 참조자) (0) | 2022.01.05 |