2016년 10월 19일 수요일

std::forward의 정체

NOTE: http://en.cppreference.com/w/cpp/utility/forward

std::forward<T>는 C++11에 추가된 템플릿 함수인데 언뜻보면 전달한다의 느낌 정도인데 막상 언제 써야하는지는 와닿지 않는다.

template <class T>
T&& forward(typename std::remove_reference<T>::type & t);
template <class T>
T&& forward(typename std::remove_reference<T>::type && t);
std::forward 함수는 위와 같이 두가지 버전이 있다. 인자의 타입 T에 따라 선택된다. 즉, 인자가 lvalue 이면 첫번째, 그렇지 않으면 두번째 버전이 선택되게 된다.

우선 forward가 하는 일은 함수가 호출될 때 인자가 가지고 있었던 타입 정보를 그대로 다른 함수 호출 때 넘겨준다.
공식적인 정의는 아니지만 경험상 이해하기 쉬운 설명이다. 여기서 말하는 타입정보는 정확히는 value category인데 C++ 표준에서 lvalue, prvalue, xvalue 등으로 분류한다.

타입추론

 먼저 함수 템플릿 인자 추론과정에서 추론을 시작하기 전에 함수 인자 타입P와 함수 호출 인자 A에 대해 몇가지 조정을 한다.
1. P가 참조 타입이 아니면
  a) A가 배열이면 포인터 타입으로 변환
  b) A가 함수타입이면 포인터 타입으로 변환
  c) A가 cv 한정사가 붙었다면 최상위의 cv한정사는 무시
template <class T> void f(T);
int a[3];
f(a);  // 1-a) P=T, A=int[3] -> int* : T=int*
const int b = 13;
f(b);  // 1-c) P=T, A=const int -> int : T=int
void g(int);
f(g);  // 1-b) P=T, A=void(int) -> void(*)(int) : T=void(*)(int)
2. P가 cv한정사가 붙은 타입이면 최상위 cv무시
3. P가 참조이면 P에 의해 참조되고 있는 타입을 사용
4. P가 cv한정사가 없는 rvalue 참조이고 함수호출에 넘겨진 인자가 lvalue이면 A대신 A의 참조 타입을 사용 (c++ standard 14.8.2.1)
template <class T> int f(T&&);  // P=cv한정사없는 rvalue참조
template <class T> int g(const T&&);  // P=cv 있는 rvalue 참조
int i;
f(i);  // P=T&&->T,A=int->int& : int f<int&>(int&)
f(0);  // P=T&& -> T,A=int : int f<int>(int&&)
//g(i);  // P=const T&&->T&&->T,A=int : int g<int>(const int &&), i(lvalue)를 const int &&에 바인딩 불가
이 과정이 끝났을 때 함수 파라미터 타입 P=A가 되게 하는 타입들을 찾는 추론과정을 거친다.

std::forward 인스턴스

cppreference에 forward함수에 대한 예제의 일부다.

class X {
public:
  X(int&& n) { std::cout << "rvalue overload, n=" << n << "\n"; }
  X(int& n)  { std::cout << "lvalue overload, n=" << n << "\n"; }
};

template<class T, class U>
std::unique_ptr<T> make_unique1(U&& u)
{
    return std::unique_ptr<T>(new T(std::forward<U>(u)));
}
 
int main()
{   
    auto p1 = make_unique1<X>(2); // rvalue
    int i = 1;
    auto p2 = make_unique1<X>(i); // lvalue
}
make_unique1이 인스턴스화 되는 과정을 보면
make_unique1(1);  // P=U&&->U,A=int : make_unique1<X, int>(int&&)
make_unique1(i);  // P=U&&->U,A=int->int&, make_unique1<X, int&>(int&)
(위 마지막에 U=int&로 추론되어 make_unique1<X, int&>(int& &&);가 되나 reference collapsing rule(c++ standard 8.3.2)에 의해 int&가 되었다)

 위와 같이 인스턴스와 될 경우 forward함수는 각각
int&& forward(int&&);  //make_unique1<X,int>(int&&)
int& forward(int&);  //make_unique1<X,int&>(int&)
가 호출되게 된다. make_unique1함수 내에서는 파라미터 u는 호출시점의 인자의 본래 value category를 상실하게되지만 템플릿 인스턴스화 과정으로 본래 타입정보를 forward를 통해 유지하고 그 타입 정보를 통해 내부에서 호출하는 다른 함수들에 대해 적절한 overloading(여기서는 std::unique_ptr<T>)이 선택될 수 있도록 해주는 역할을 한다.

 당연하겠지만 forward함수자체는 뭔가 특별한 일을 하는게 아니라 forward의 인자타입과 리턴타입 정보를 유지해주는 부분이 핵심인 것이다. 실제로 forward함수 구현은 달랑 한줄이다.(libstdc++ 5.3.1)
template <class T>
T&& forward(typename std::remove_reference<T>::type & t)
{
  return static_cast<T&&>(t);
}
template <class T>
T&& forward(typename std::remove_reference<T>::type && t)
{
  return static_cast<T&&>(t);
}

언제 forward를 써야 하나

1. 함수 템플릿을 정의하려고 한다.
2. 이 함수 템플릿 내부에 어떤 함수 X를 호출하는데 X의 인자 타입이 rvalue참조인것과 그렇지 않은 여러버전의 오버로딩이 존재하고 우리 함수의 인자에 따라 X의 오버로딩중 적절한 것을 선택해 호출할 필요가 있다.

위 조건을 만족한다면 주저말고 forward를 써야한다.

댓글 없음:

댓글 쓰기