1. 함수 템플릿 매개 변수의 타입이 T&& 형태이고 T가 추론된다면, 또는 객체를 auto&& 로 선언한다면, 그 매개변수나 객체는 보편 참조이다.
보편 참조는 우측값, 좌측값은 물론 const 객체, 비 const 객체, volatile 객체, 비 volatile 객체, 심지어 const 이자 volatile 인 객체에도 묶을 수 있다.
2. 타입 선언의 형태가 정확히 타입 &&가 아니면, 또는 타입 추론이 일어나지 않으면, 타입 &&은 우측값 참조를 뜻한다.
void f(Widget&& param); // 우측값 참조
Widget&& var1 = Widget(); // 우측값 참조
auto&& var2 = var1; // 우측값 참조 아님
template <typename T>
void f(std::vector<T>&& param); // 우측값 참조
template <typename T>
void f(T&& param); // 우측값 참조 아님
- 어떤 타입 T에 대한 우측값 참조를 선언할 때에는 T&&라는 표기를 사용한다. 하지만, 소스 코드에서 T&& 를 발견했다고 해서 그것이 항상 우측값 참조라고 가정하면 안 된다. 형태가 T&& 이고 T가 추론되는 경우와, auto&&로 객체를 선언하는 경우에, 그 매개 변수나 객체는 보편 참조(universal reference) 이다.
template <typename T>
void f(T&& param); // param은 보편 참조(universal reference)
// 다른 한 문맥은 auto 선언이다.
auto&& var2 = var1; // var2는 보편 참조(universal reference)
- 보편 참조는 두 가지 문맥에서 나타난다. 가장 흔한 것은 함수 템플릿 매개 변수이다. 이 두 문맥의 공통점은 타입 추론이 일어난다는 점이다. 템플릿 f에서는 param의 타입이, var2의 선언에서는 var2의 타입이 추론된다.
void f(Widget&& param); // 타입 추론 없음 param은 우측값 참조
Widget&& var1 = Widget(); // 타입 추론 없음 var1은 우측값 참조
- 타입 추론이 일어나지 않는 문맥에서 &&는 우측값 참조이다.
- 하나의 참조가 보편 참조 이려면 반드시 타입 추론이 관여해야 한다. 그런데 이는 필요조건일 뿐 충분 조건은 아니다. 참조 선언의 형태도 정확해야 한다. 구체적으로 딱 T&& 의 형태이어야 한다.
template <typename T>
void f(std::vector<T>&& param); // param은 우측값 참조
- f 호출시 타입 T가 추론된다. 그러나 param의 타입 추론의 형태가 T&&가 아니라 std::vector<T>&& 이다. 이 때문에 param은 보편 참조가 될 수 없다. (우측값 참조이다.)
std::vector<int> v;
f(v); // 오류! 좌측값을 우측값 참조에 묶을 수 없음
- f 호출 시 타입 T가 추론된다. 그러나 param의 타입 추론의 형태가 T&&가 아니라 std::vector<T>&& 이다. 이 때문에 param은 보편 참조가 될 수 없다(우측값 참조.)
- 그리고 그냥 const 한정사 하나만 붙여도 참조는 보편 참조가 되지 못한다.
template <typename T>
void f(const T&& param); // param은 우측값 참조
- 그런데 T&&라고 다 보편 참조는 아니다. 예를들어 템플릿 안에 타입이 T&&인 함수 매개 변수를 발견했을 때, 그것이 반드시 보편 참조라고 확실할 수 는 없다. 템플릿 안에서는 타입 추론이 반드시 일어난다는 보장이 없기 때문이다.
template <class T, class Allocator = allocator<T> >
class vector {
public:
void push_back(T&& x);
...
};
- push_back의 매개 변수는 확실히 보편참조가 요구하는 형태이지만, 이 경우에는 타입 추론이 전혀 일어나지 않는다. push_back는 반드시 구체적으로 인스턴스화된 vector의 일부이어야 하며, 그 인스턴스의 타입은 push_back의 선언을 완전하게 결정하기 때문이다.
std::vector<Widget> v;
// -----
class vector<Widget, allocator<Widget> > {
public:
void push_back(Widget&& x); // 우측값 참조
...
};
- 예를들어 std::vector 템플릿은 다음과 같이 인스턴스화 된다.
- 반면, std::vector의 멤버 함수들 중 이와 개념적으로 비슷한 emplace_back 멤버 함수는 실제로 타입 추론을 사용한다.
template <class T, class Allocator = allocator<T> >
class vector {
public:
template <class... Args>
void emplace_back(Args&&... args);
...
};
- 이 경우 타입 매개 변수 Args 는 vector의 타입 매개 변수 T와 독립적이다. 따라서 Args는 emplace_back이 호출될 때마다 추론되어야 한다.
- 즉, args는 보편 참조이다. 이 예를 보면, 보편 참조의 형태가 T&& 라는 말에는 타입이름 T가 꼭 T이어야 할 필요는 없다는 것을 알 수 있다.
- 즉, 다음과 같은 경우도 보편 참조이다.
template <typename MyTemplateType>
void someFunc(MyTemplateType&& param); // param은 보편 참조(universal reference)
- 앞에서 auto&&를 타입으로 해서 선언된 변수도 보편 참조라고 했었다. 타입 추론이 일어났고 형태도 T&& 이기 때문이다. auto 보편 참조는 C++11보다 C++14에서 더 자주 볼 수 있다. C++14의 람다 표현식에서 auto&& 매개 변수를 사용할 수 있기 때문이다.
auto timeFuncInvocation = [](auto&& func, auto&&... params)
{
std::forward<decltype(func)>(func)(
std::forward<decltype(params)>(params)...
);
}
- std::forward<decltype(~~)> 가 사용된 이유는 항목33 에서 찾을 수 있다. timeFuncInvocation을 이용하면 거의 모든 함수의 실행 시간을 측정할 수 있다. 모든 함수가 아니라 거의 모든 함수인 이유는 항목30 에서 찾을 수 있다.
3. 우측값으로 초기화되는 보편 참조는 우측값 참조에 해당한다. 좌측값으로 초기화되는 보편 참조는 좌측값 참조에 해당한다.
template <typename T>
void f(T&& param); // param은 보편 참조(universal reference)
Widget w;
f(w); // f에 좌측값이 전달됨; param의 타입은 Widget&(즉, 좌측값 참조)
f(std::move(w)); // f에 우측값이 전달됨; param의 타입은 Widget&&(즉, 우측값 참조)
'PROGRAMMING > Effective Modern C++' 카테고리의 다른 글
23. std::move와 std::forward를 숙지하라. (0) | 2021.03.13 |
---|---|
22. Pimpl 관용구를 사용할 때에는 특수 멤버 함수들을 구현파일에서 정의하라. (0) | 2021.03.13 |
21. new를 직접 사용하는 것보다 std::make_unique와 std::make_shared를 선호하라. (0) | 2021.03.12 |
20. std::shared_ptr 처럼 작동하되 대상을 잃을 수 도 있는 포인터가 필요하면 std::weak_ptr을 사용하라. (0) | 2021.03.11 |
19. 소유권 공유 자원의 관리에는 std::shared_ptr을 사용하라. (0) | 2021.03.09 |
댓글