1. 정의
void f(widget&& param);
Widget&& var1 = someWidget; // rvalue ref
auto&& var2 = var1; // rvalue ref or lvalue ref (universal ref)
template <typename T>
void f(std::vector<T>&& param); // rvalue ref
template <typename T>
void f(T&& param); // rvalue ref or lvalue ref (universal ref)
2. Universal reference
- 변수나 인자가 type deduction(추론)이 필요하며, 정확히 "T&&"로 선언되면, 해당 변수나 인자는 universal reference이다.
- 변수가 타입 추론이 필요한 경우는 auto로 선언된 변수이고, 인자가 타입 추론이 필요한 경우는 템플릿 함수의 인자인 경우다. 참고로, 위 둘의 타입 추론 규칙은 완벽하게 동일하며, 템플릿 함수의 인자로 훨씬 많이 사용된다.
2-1. auto&&
Universal reference는 추론될 타입이 rvalue reference인가? lvalue reference인가?에 따라 &&를 해석한다. var1의 주소를 알 수 있으므로, var1 자체(변수 그 자체)는 lvalue이다. var2의 타입은 auto&&로 선언되었기에 universal reference이고, var1이 lvalue이므로 var2는 lvalue reference로 해석된다.
Widget&& var1 = someWidget;
auto&& var2 = var1; // 즉 Widget& var2 = var1;
std::vector<int> v;
auto&& val = v[0];
2-2. T&& as template function argument
template <typename T>
void f(T&& param);
// 주소를 취할 수 없는 literal 10은 rvalue
// 따라서, param은 rvalue reference (int&&)
f(10);
// 주소를 취할 수 있는 x는 lvalue
// 따라서, param은 lvalue reference (int&)
int x = 10;
f(x);
template <typename T>
void f(T&& param); // T에 대한 타입 추론이 필요. && ≡ universal reference
template <typename T>
class Widget
{
...
Widget(Widget&& rhs); // 명시적인 Widget 타입. && ≡ rvalue reference
};
template <typename T1>
class Gadget
{
...
template <typename T2>
Gadget(T2&& rhs); // T2에 대한 타입 추론이 필요. && ≡ universal reference
};
void f(Widget&& param); // 명시적인 Widget 타입. && ≡ rvalue reference
2-3. 혼동하기 쉬운 경우들
template <typename T>
void f(std::vector<T>&& param); // rvalue ref
template <typename T>
void f(const T&& param); // rvalue ref
또한, T가 템플릿 인자이고, 템플릿 함수 인자가 T&&여도 T에 대한 타입 추론이 발생하지 않는 경우도 있다. push_back 함수는 T&&를 함수 인자로 받는다. 하지만, push_back 함수는 std::vector의 객체가 존재하지 않으면 호출될 수 없다. 즉, std::vector<T>의 객체가 생성되기 위해, instantiate 되는 과정에서 T는 이미 타입 추론이 완료되었기에, push_back이 호출되는 시점에서 다시 T에 대한 타입 추론을 할 필요가 없는 것이다. 따라서, push_back의 "T&&"는 universal reference가 아닌, rvalue reference 이다.
template <typename T, type Allocator = allocator<T> >
class vector
{
public:
...
void push_back(T&& x); // 이미 T의 타입을 알기에, 타입 추론이 발생하지 않는다. && ≡ rvalue reference
};
// Widget 객체를 생성하는 팩토리 함수
Widget makeWidget();
// Widget 벡터 생성
std::vector<Widget> vw;
Widget w;
// 이 함수를 풀어쓰면 다음과 같다.
// std::vector<Widget>(Widget&& w);
// 이미 이 시점에서 Widget 타입이라는 것이 명시되어 있으므로, 타입 추론할 필요가 없다.
vw.push_back(makeWidget());
push_back과 대조적으로 emplace_back의 경우는 다음과 같이 정의되어 있다. emplace_back 함수는 variadic template 함수로 구현되어 있어, 가변수의 인자를 받는다. 따라서, 가변수의 개별 인자 타입들은 모두 타입 추론이 필요하다.
template <typename T, typename Allocator = allocator<T> >
class vector
{
public:
...
template <class... Args>
void emplace_back(Args&&... args); // 각 Args들의 타입 추론이 필요. && ≡ universal references
};
template<class... Args>
void std::vector<Widget>::emplace_back(Args&&... args);
2-4. lvalueness or rvalueness
// Widget을 생성하는 팩토리 함수
Widget makeWidget();
// var1의 타입은 rvalue reference이지만, var1 자체는 lvalue
Widget&& var1 = makeWidget();
// 여기에서의 var1은 lvalue
// var2의 타입은 rvalue reference이지만, var2 자체는 lvalue
Widget&& var2 = std::move(var1);
rvalue reference 타입이지만, 이름이 붙어 있는 변수나 인자들은 주소를 획득할 수 있기에 lvalue 들이다.
template <typename T>
class Widget
{
...
Widget(Widget&& rhs); // rhs의 타입은 rvalue reference,
... // 하지만, rhs 자체는 lvalue
};
template<typename T1>
class Gadget
{
...
template <typename T2>
Gadget(T2&& rhs); // rhs의 타입은 universal reference
... // 하지만, rhs 자체는 lvalue
};
3. Reference Collapsing
Widget w1;
// 에러! 레퍼런스의 레퍼런스 따윈 없는거임!
Widget& & w2 = w1;
3-1. Template function arguments
template<typename T>
void f(T&& param);
...
int x;
...
// rvalue로 f 함수 호출
f(10);
// lvalue로 f 함수 호출
f(x);
숫자 리터럴 10으로 f 호출시, T는 int로 추론되며 f 함수는 다음과 같이 instantiate된다.
// rvalue로부터 f 함수 instantiated
void f(int&& param);
하지만, 변수 x, lvalue로 f 호출시 T는 int&로 추론되며, f 함수는 레퍼런스의 레퍼런스를 포함한 채 instantiate된다.
// lvalue로부터 f 함수 instantiated
void f(int& && param);
- & & (L + L)
- & && (L + R)
- && & (R + L)
- && && (R + R)
- && && (R + R) 은 &&로 collapse 된다.
- 그 외 나머지 조합(L 이 하나라도 포함된)은 &로 collapse 된다.
- & & (L + L) -> &
- & && (L + R) -> &
- && & (R + L) -> &
- && && (R + R) -> &&
// reference collapsing 이후 lvalue로부터 f 함수 instantiated
void f(int& param);
template<typename T>
void f(T&& param);
int x;
// r1의 타입은 int&&
int&& r1 = 10;
// r2의 타입은 int&
int& r2 = x;
f(r1);
f(r2);
3-2. auto variable
// var1은 Widget&& 타입
Widget&& var1 = someWidget;
auto&& var2 = var1;
// reference-stripping으로 인해 먼저 var1은 대입되기 전 Widget 타입
// Widget var1은 lvalue이므로, lvalue reference로 타입 추론
Widget& && var2 = var1;
// 최종적으로 다음과 같이 reference collapsing이 발생
Widget& var2 = var1;
3-3. typedef
세번째 reference collapsing이 발생하는 경우는 typedef를 사용하는 경우이다.
template <typename T>
class Widget
{
typedef T& LValueRefType;
...
};
Widget<int&> w
객체 w는 다음과 같이 instantiate 된다.
// instantiate 된 직후의 typedef
typedef int& & LValueRefType;
// reference-collapsing이 발생한 이후
typedef int& LValueRefType;
이 상태에서 다음과 같이 Widget을 사용하게 되면..
void f(Widget<int&>::LvalueRefType&& param);
이는 다음과 같이 해석되고,
void f(int& && param);
최종적으로 다음과 같이 reference collapsing이 발생한다.
void f(int& param);
3-4. decltype
Widget w1, w2;
// v1은 lvalue로부터 초기화되는 auto-based universal reference
// 따라서, v1의 타입은 w1을 참조하는 lvalue reference
// Widget& v1 = w1;과 동일하다
auto&& v1 = w1;
// v2는 decltype-based universal reference
// w2가 어떤 타입인지 관계없이, decltype(표현식)의 표현식에만 의존해 타입이 결정된다
// decltype(w1)은 Widget이므로(Widget&이 아님), v2의 타입은 rvalue reference
// 즉, Widget&& v2 = w2;와 동일하다.
// 하지만, w2는 lvalue
// rvalue reference를 lvalue로 초기화할 수 없으므로 아래 코드는 컴파일 에러 발생
// decltype(w1)&& v2 = std::move(w2);로 해야 적법한 코드가 된다
decltype(w1)&& v2 = w2;
'프로그래밍 언어 > C++' 카테고리의 다른 글
C언어로 객체지향 주 4개의 요소 (추상화,다형성,상속,캡슐화) 구현하기 (0) | 2022.11.26 |
---|---|
C++ call by value, call by reference (0) | 2022.11.17 |
C++ 공용체(union) 개념과 통신에서의 사용 이유 (0) | 2022.11.10 |
C++ struct(구조체), union(공용체) 크기에 대한 정리 (0) | 2022.11.10 |
C++ 함수 객체 (Functor) / () 연산자 오버로딩 (0) | 2022.11.08 |