본문 바로가기
PROGRAMMING/Effective Modern C++

24. 보편 참조(universal reference)와 우측값 참조를 구별하라.

by La-KanTo 2021. 9. 5.

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&&(즉, 우측값 참조)

 

댓글