Item 24 - universal reference
마지막 수정 시각: 2020-10-26 22:03:12
어떤 타입 T에 대한 rvalue reference는 T&&로 나타낸다. 따라서 소스 코드에 나타나는 T&&들은 모두 T 타입에 대한 rvalue reference로 봐도 될 것이다. 진짜 그럴까? 안타깝게도 실제로는 전혀 그렇지 않다.
//rvalue reference
void f(Widget&& param);
//rvalue reference
Widget&& var1 = Widget();
//not rvalue reference
auto&& var2 = var1;
//rvalue reference
template<typename T>
void f (std::vector<T>&& param);
//not rvalue reference
template<typename T>
void f(T&& param);
T&& 는 두 가지의 의미를 갖고 있다. 하나는 rvalue reference, 또다른 하나는 해당 타입이 rvalue reference 거나 lvalue reference라는 것이다(즉, T& 거나 T&& 둘 모두 가능하다는 것). 심지어는 이건 const냐 c아니냐 volatile이냐 아니냐까지 모두 상관하지 않고 받아들인다. 사실상 거의 어떤 타입이든 다 가능하다고 봐도 되는 것이다. 이런 성격 때문에 이름 짓기 좋아하는 meyers는 이걸 universal reference라고 부른다.
universal reference가 나타나는 경우
universal reference는 두 가지 상황에서 발생한다. universal reference가 나타나는 가장 일반적인 상황은 함수 템플릿의 인자로 사용되는 상황이다.
//param은 universal reference
template<typename T>
void f(T&& param);
또 다른 상황은 auto 타입 추론이다.
//var2는 universal reference
auto&& var2 = var1;
결국 universal reference는 일반적으로 타입 추론을 하는 상황에서 나타난다는 것이다. 첫번째 함수 f에서 param의 타입이 추론되고, 두 번째 var2 = var1;에서 var2의 타입이 추론된다. 아래와 같이 타입 추론 없이 &&을 쓰는 경우에는 명백하게 rvalue reference이다.
//이 경우는 명백하게 rvalue reference이다.
void f(Widget&& param);
Widget&& var1 = Widget();
universal reference 역시 참조이므로 반드시 초기화가 되어야한다. 이 초기화가 universal reference가 lvalue reference가 될 지 rvalue reference가 될 지를 결정한다. 초기화할 때 넘어오는 인자에 따라 lvalue reference가 될 수도 rvalue reference가 될 수도 있는 것이다.
template<typename T>
void f(T&& param); //param은 universal reference
Widget w;
f(w); //w = lvalue이므로 param의 타입은 Widget&
f(std::move(w)); //std::move(w)는 rvalue이므로 param의 타입은 Widget&&.
universal reference가 나타나기 위해 타입 추론이 필요한 것은 사실이지만 그걸로 충분하진 않다. universal reference가 나타나기 위해서는 반드시 레퍼런스의 형태가 T&&이어야 한다. 이게 무슨 뜻인지 예제를 통해 살펴보자.
//이 경우 param의 타입은 반드시 rvalue reference이다!
template<typename T>
void f(std::vector<T>&& param);
위와 같은 코드에서 f가 실행될 때 T의 타입이 추론되긴 하지만, param의 타입이 T&&
가 아니라 std::vector<T>&&
이기 때문에 이건 universal reference가 될 수 없다. 이 경우 param의 타입은 무조건 rvalue reference가 된다. 그래서 아래와 같은 현상이 발생한다.
std::vector<int> v;
//error! lvalue를 rvalue reference로 바인딩할 수 없다.
f(v);
심지어는 const같은 미묘한 제약정도만 붙어도 universal reference는 발생하지 않는다.
//param은 무조건 rvalue reference 타입.
template<typename T>
void f(const T&& param);
좋아, 그럼 템플릿 안에 있는 T&&는 모두 universal reference로 보면 되겠군! 이라고 생각할 수 있지만 꼭 그렇지만은 않다는 점도 명심해야한다. 템플릿 안에 있는 T&&지만 타입 추론이 일어나지 않는 경우도 존재한다.
//std::vector의 구현 일부
template<class T, class Allocator = allocator<T> >
class vector
{
public:
...
void push_back(T&& x);
...
};
위 코드에서 push_back은 T&&를 인자로 받는다. 그럼 T&&는 universal reference겠네? 그렇지 않다. T&&는 rvalue reference다.
std::vector<Widget> v;
//위 선언에 의해 구체화되는 클래스
class vector<Widget, allocator<Widget> >
{
public:
//rvalue reference
void push_back(Widget&& x);
};
위 코드와 같이, vector가 구체화될 때 push_back의 인자도 같이 정해지기 때문에 push_back을 호출할 때 타입 추론이 일어나는 것이 아니다. 반면 vector의 emplace_back 멤버 함수의 경우 타입 추론이 일어난다.
template<class T, class Allocator = allocator<T> >
class vector
{
public:
...
template<class... Args>
void emplace_back(Args&&... args);
...
};
emplace_back이 받는 Args의 경우 타입 T와는 무관하다. 따라서 Args의 경우 타입 추론이 일어나고, 이 형태는 T&& 형태와 완전히 같으므로(parameter pack은 일단 무시) 이건 universal reference가 되는 것이다.
그리고 위에서 언급한 auto&&의 경우도 마찬가지로 타입 추론이 일어나며, T&& 형태와 완전히 같으므로 universal reference가 되는 것이다. C++11에서는 이걸 쓸 일이 그렇게 많진 않지만 C++ 14부터는 달라진다. C++14에서 람다의 매개변수로 auto&&를 쓸 수 있기 때문이다.
auto timeFuncInvocation =
[](auto&& func, auto&&... params)
{
beginTimer();
std::forward<decltype(func)>(func)
(std::forward<decltype(params)>(params)...);
endTimer();
}
이런식의 구현이 가능하기 때문에 굉장히 유용하게 쓸 수 있다.