C++11부터 rvalue reference를 지원하는 것은 이미 공부해서 알고 있었다. 그런데 rvalue reference만 공부하면 자칫 '&&가 붙으면 무조건 rvalue reference다' 라는 오류에 사로잡힐 수 있다. 사실은 그게 아니다. &&가 붙어도 lvalue reference인 경우도 있기 때문이고 그것이 universal reference의 경우이다.
universal reference는 &&로 선언된 변수나 인자가 타입 추론이 필요한 경우를 의미하며 이때는 rvalue/lvalue reference 모두 가능하다. 스콧 마이어씨의 글에는 다음과 같이 적혀있다.
If a variable or parameter is declared to have type T&& for some deduced type T, that variable or parameter is a universal reference.
타입 추론이 필요한 경우는 아래 두 경우다.
1. 변수가 auto로 선언된 경우
2. 템플릿 함수의 인자인 경우
1. 변수가 auto&& 타입인 경우
모든 레퍼런스 변수처럼 auto&&도 선언과 동시에 초기화가 필요하다.
Widget && var1 = someWidget;
auto && var2 = var1;
위의 경우 var1자체는 주소를 갖고 있기 때문에 lvalue이고, Widget&& var1 = someWidget; 식 자체에서 &&는 rvalue reference를 의미한다. 이 경우 var2는 lvalue를 참조하고 있기 때문에 lvalue reference이다. &&가 붙어도 lvalue reference인 경우다.
사실 위의 2줄의 코드는 아래 한줄의 코드와 같은 의미다.
Widget& var2 = var1;
2. 템플릿 함수의 인자가에 &&가 붙어있는 경우
auto의 경우보다 훨씬 자주 쓰이는 경우라고 한다.
template<typename T>
void f(std::vector<T>&& param); // here, “&&” means rvalue reference
template<typename T>
void f(T&& param); // here, “&&”does not mean rvalue reference
첫번째 경우는 헷갈리기 쉬운 케이스로써, 인자의 타입은 T에 대한 &&가 아니라 std::vector<T>에 대한 &&이다. 즉 universal reference가 아니라 rvalue reference이다.
두번째 케이스는 넘겨주는 인자 param이 무엇인지에 따라서 lvalue reference가 될 수도 있고, rvalue reference가 될 수도 있다. 예를 들어서 아래 예시에서 볼 수 있듯이 f(a); 처럼 인자 param으로 lvalue를 넘겨주면 lvalue reference, f(3); 처럼 인자 param을 rvalue로 넘겨주면 rvalue reference인것이다. &&가 붙어도 lvalue/rvalue reference 모두 될 수 있는 것이다(universal reference).
template <typename T>
void f(T&& param)
{
std::cout << "Universal reference" << std::endl;
}
int main()
{
int a = 3;
f(a); // lvalue reference
f(3); // rvalue reference
return 0;
}
3. Universal reference인지 Rvalue reference인지 헷갈리는 경우
template<typename T>
void f(std::vector<T>&& param); // “&&” means rvalue reference
// ****************************************************************
template<typename T>
void f(const T&& param); // &&앞에 const만 붙어도 universal이 아니라 rvalue
// ****************************************************************
template <class T, class Allocator = allocator<T> >
class vector {
public:
...
void push_back(T&& x);
... // && ≡ rvalue reference
};
마지막 push_back같은 경우 rvalue reference가 되는 이유는 type deduction이 일어나지 않아도 되기 때문이다. type deduction이 일어나지 않아도 되는 이유는, 이미 vector의 push_back을 호출하기 위해선 vector를 먼저 인스턴스화 해야하고, vector를 인스턴스화 했을 때 T가 무슨 타입인지 알수 있기 때문에 push_back 할 때 T타입에 대한 추론이 필요 없다는 것이다. 예를 들어
vector<int> vec;
vec.push_back(3);
vec이란 vector를 만들때 이미 타입 T가 int라는걸 안다. 따라서 push_back을 호출할 때는 타입에 대한 추론이 필요 없으므로 rvalue reference다. 하지만 emplace_back은 경우가 다르다.
template <class T, class Allocator = allocator<T> >
class vector {
public:
...
template <class... Args>
void emplace_back(Args&&... args); // deduced parameter types ⇒ type deduction;
... // && ≡ universal references
};
emplace_back은 vector가 인스턴스화 될 때 타입 T를 알아도, 타입 T는 emplace_back의 인자(들)의 타입 Args와 독립적이다. 따라서 vector가 인스턴스화 됐을 때 타입을 미리 알 수 없으므로 emplace_back함수에서 인자 추론을 해야하고 따라서 emplace_back의 인자에 &&는 universal reference이다.
4. std::forward 함수
이전에 공부했던 std::move같은 경우에는 단순히 lvalue를 rvalue로 바꿔주기만 하는 기능을 했다. std::forward도 마찬가지로 캐스팅만 하는데 인자로 lvalue reference를 넘겨주면 그대로 lvalue reference를 리턴하고, lvalue reference가 아닌 것을 넘겨주면 rvalue reference를 리턴한다(코드 출처: https://jungwoong.tistory.com/53)
// lvalue를 캐치하는 함수
void Catch(Person& p, const char* name)
{
cout << name << "lvalue catch" << endl;
}
// rvalue를 캐치하는 함수
void Catch(Person&& p, const char * name)
{
cout << name << "rvalue catch" << endl;
}
// 전달받은 obj를 std::forward를 통해서 catch 함수로 전달합니다.
template<typename T>
void ForwardingObj(T&& obj, const char* name)
{
Catch(std::forward<T>(obj), name);
}
int _tmain(int argc, _TCHAR* argv[])
{
Person p1("ahn", 1985);
ForwardingObj(p1, "p1\t\t=\t");
ForwardingObj(std::move(p1), "std::move(p1)\t=\t");
return 0;
}
ForwardingObj의 T&& obj는 universal reference이다. 1번째 ForwardingObj는 첫번째 인자로 lvalue를 넘겨주고 있으므로 ForwadingObj는 내부에서 Catch함수를 호출할 때 obj를 그대로 lvalue reference로 넘겨주고 결과적으로 lvalue를 캐치하는 Catch함수를 호출한다. 2번째 ForwardingObj는 std::move를 이용해서 p1을 rvalue로 casting하고 ForwadingObj안에서 std::forward를 이용해서 obj를 rvalue reference로 casting하고 결국 rvalue를 캐치하는 Catch함수를 호출한다.
5. Reference Collapsing
컴파일러가 T&& universal reference의 타입을 추론하는 경우에 lvalue reference가 되는 경우는 T&로 추론되고, rvalue reference로 추론되는 경우는 그냥 T( T&&r가아님 )로 추론된다. 그런데 함수의 파라미터 타입과 넘겨준 인자가 조합되어 &가 2개 이상 되는 경우도 볼 수 있다. 예를 들어
template<typename T>
void f(T&& param);
...
int x;
...
// rvalue로 f 함수 호출
f(10);
// lvalue로 f 함수 호출
f(x);
위의 경우
f(10) // void f(T&& param)로 추론
f(x) //void f(T&& ¶m)로 추론.
// universal reference는 lvalue reference로 추론
// 추론하는 과정에서 1차적으로는 &가 3개인 상태로 추론 될 것이다.
// 물론 이렇게 추론된 universal reference는 아래의 규칙을 통해 collapsing 되어야 한다.
이렇게 추론될 것이다.
1차적으로 위와 같이 추론된 인자들은 아래와 같은 규칙을 통해 reference collapsing이라고 하는 과정을 거친다. collapsing의 사전적 의미는 '무너지다', 혹은 '붕괴하다' 라는 의미다.
가능한 lvalue reference와 rvalue reference의 경우를 조합하면 아래와 같이 4가지 경우가 나온다.
- & & (L + L) -> &
- & && (L + R) -> &
- && & (R + L) -> &
- && && (R + R) -> &&
위의 코드에서 f(x)경우 void f(T& param)으로 reference collapsing을 통해 최종적으로 추론될 것 이다.
그런데 조금 특이한 경우는 레퍼런스 인자를 universal reference타입의 함수에 인자로 넘겨주는 경우다. 예를 들어보면
template<typename T>
void f(T&& param);
int x;
int&& r1 = 10;
int& r2 = x;
f(r1);
f(r2);
이때는 특이하게도 f(r1), f(r2) 모두 레퍼런스가 무시되어서 lvalue reference(r1, r2 변수 자체는 lvalue들이니까)로 int&로 간주된다. 이걸 가리켜 'reference-stripping'된 채로 처리된다고 부른다.
auto나, typedef를 사용한 코드의 universal reference의 reference collpasing에 의한 추가적인 예시나 설명은 레퍼런스에 설명이 잘 되어있으므로 참고하면 좋을듯하다.
Reference
https://isocpp.org/blog/2012/11/universal-references-in-c11-scott-meyers
'공부 > C || C++' 카테고리의 다른 글
C 입력 버퍼 비우기 (0) | 2021.07.30 |
---|---|
C++ iterator를 reverse_iterator로 변환시 같은 element를 가리키지 않는 이유 (0) | 2021.07.30 |
C++ 11 함수 객체(Functor)와 람다 표현식(Lambda Expression) (0) | 2021.07.24 |
C++ 템플릿 특수화 (0) | 2021.07.24 |
C++ 템플릿 클래스/함수 헤더파일에 선언과 정의 모두 해줘야 하는 이유 (0) | 2021.07.23 |