Creative Motive

Rvalue Reference #4 - Perfect Forwarding 본문

C++

Rvalue Reference #4 - Perfect Forwarding

aicosmos 2013. 2. 22. 13:47

데브피아 김경진님 작성 (http://devmachine.blog.me/177199502)


Forwarding Problem

이번 강좌는 이해를 돕기 위해 자세한 설명 보다는 예제 위주로 설명을 드리려고 합니다. 쉽지 않은 개념이긴 하지만 차근 차근 따라오신다면 모두 이해가 될것으로 생각됩니다. 그럼 시작하겠습니다. ^^

먼저 구조체 W, X, Y, Z를 정의하는데 각각의 구조체는 생성자의 파라미터 타입으로 const와 non-const 참조 타입을 받아들이는 4가지 조합을 가지게 합니다.

struct W
{
W(int&, int&) {}
};

struct X
{
X(const int&, int&) {}
};

struct Y
{
Y(int&, const int&) {}
};

struct Z
{
Z(const int&, const int&) {}
};

그 다음 전달받은 타입에 해당되는 객체를 생성해주는 템플릿 함수를 만들어보도록 하겠습니다.

template <typename T, typename A1, typename A2>
T* factory(A1& a1, A2& a2)
{
return new T(a1, a2);
}

그럼 이제 factory 함수를 이용하여 객체를 생성하는 코드를 작성해볼까요?

int a = 4, b = 5;
W* pw = factory<W>(a, b); // 정상적으로 컴파일됨
Z* pz = factory<Z>(2, 2); // non-const reference 타입에 Rvalue를 전달할 수 없으므로 컴파일 오류 발생

factory 함수는 non-const reference 타입을 파라미터로 받습니다. W 객체를 생성하는 코드는 Lvalue인 변수 a, b를 파라미터로 사용했기 때문에 정상적으로 컴파일 되지만 Z 객체를 생성하는 코드는 Rvalue인 상수 2가 non-const reference로 전달될 수 없기 때문에 컴파일 오류가 발생합니다. 이 문제를 해결하기 위해서는 다음과 같이 A& 타입과 const A& 타입을 조합한 4개의 오버로드된 템플릿 함수를 제공해야 합니다.

template <typename T, typename A1, typename A2>
T* factory(A1& a1, A2& a2)
{
return new T(a1, a2);
}

template <typename T, typename A1, typename A2>
T* factory(const A1& a1, A2& a2)
{
return new T(a1, a2);
}

template <typename T, typename A1, typename A2>
T* factory(A1& a1, const A2& a2)
{
return new T(a1, a2);
}

template <typename T, typename A1, typename A2>
T* factory(const A1& a1, const A2& a2)
{
return new T(a1, a2);
}

이번 예제에서는 파라미터의 개수가 2개이므로 4개의 템플릿 함수만 오버로드하면 되지만 만약 파라미터의 개수가 증가한다면 오버로드 해야할 템플릿 함수의 개수는 기하급수적으로 증가합니다. 예를들어 파라미터 개수가 6개라면 무려 64개의 템플릿 함수를 오버로드해야하는 사태가 발생하게 되죠. 지금과 같이 참조 타입을 템플릿 파라미터로 사용하는 템플릿 함수에서 또 다른 함수로 파라미터를 전달할 때 발생하는 문제를 Forwarding Problem 이라고 얘기합니다.

Perfect Forwarding

Forwarding Problem은 Rvalue reference가 도입되기 이전까지는 마땅한 해결방법이 없었습니다. 무식한 방법이지만 여러개의 오버로드 버전을 제공하는것이 최선이었죠. 하지만 Rvalue reference가 도입되면서 하나의 템플릿 함수만 구현하더라도 Forwarding Problem을 해결할 수 있는 방법이 생기게 되었습니다. 그럼 factory 함수를 다시 작성해 보도록 할까요?

template <typename T, typename A1, typename A2>
T* factory(A1&& a1, A2&& a2)
{
return new T(a1, a2);
}

factory 함수는 지난 강좌 마지막에 설명했던 템플릿 파라미터의 타입 추론 규칙에 의해 Lvalue를 전달 받았을 경우 A& && -> A& 타입으로, Rvalue를 전달받았을 경우 A&& 타입으로 추론되어 Lvalue와 Rvalue를 모두 전달 받을 수 있게 됩니다. 이로서 하나의 템플릿 함수 구현만으로 모든 경우의 파라미터 타입을 처리할 수 있게 되었습니다.

하지만 아직까지는 완벽하다고 얘기할 수 없습니다. 지난 강좌에서 설명한 Rvalue Reference 속성 중에 '변수로서 이름 붙여진 Rvalue reference는 Rvalue가 아닌 Lvalue로 취급된다' 라는 규칙을 기억하실런지 모르겠네요. 이 규칙을 적용하면 파라미터로 Rvalue를 전달받았더라도 A&& 타입은 함수 내부에서는 Lvalue로 취급되기 때문에 T 생성자의 파라미터는 Lvalue로 전달됩니다. 만약 Rvalue Reference 타입을 파라미터로 전달받는 T 생성자가 존재한다면 제대로 호출되지 않겠죠.

이것은 우리가 원하는 바가 아닙니다. factory 함수로 Lvalue가 전달되면 T 생성자에 Lvalue로, Rvalue가 전달되면 T 생성자에에 Rvalue로 전달되어야 완벽한 파라미터 전달이라고 얘기할 수 있습니다. 그렇게 하기 위해서는 다음과 같이 T 생성자의 파라미터를 static_cast<A&&>로 타입 캐스팅 하여 전달하면 됩니다. 템플릿 파라미터의 타입 추론 규칙에 의해 Lvalue는 static_cast<A& &&> -> static_cast<A&>로, Rvalue는 static_cast<A&&>로 각각 타입 캐스팅되어 전달되는 마법과 같은 구현이 완성됩니다.

template <typename T, typename A1, typename A2>
T* factory(A1&& a1, A2&& a2)
{
return new T(static_cast<A1&&>(a1), static_cast<A2&&>(a2));
}

이것은 아래와 같이 std::forward 함수를 이용하면 static_cast<A&&>와 같은 기능을 하면서 좀 더 직관적인 이름으로 사용할 수 있습니다.

template <typename T, typename A1, typename A2>
T* factory(A1&& a1, A2&& a2)
{
return new T(std::forward<A1>(a1), std::forward<A2>(a2));
}

이제 factory 함수가 Lvalue를 전달받으면 T 생성자에게 Lvalue를 전달해주고, Rvalue를 전달받으면 T 생성자에게 Rvalue를 전달해주게 되었습니다. 드디어 완벽한 파라미터 전달, 즉 Perfect Forwarding이 완성되었네요. 마지막으로 다음 예제에서는 완성된 factory 함수를 이용하여 객체를 생성하는 main 함수의 구현을 보여줍니다. 하나의 factory 함수만으로도 4가지 경우의 객체를 정상적으로 생성하는 것을 볼 수 있습니다.

int main()
{
int a = 4, b = 5;
W* pw = factory<W>(a, b);
X* px = factory<X>(2, b);
Y* py = factory<Y>(a, 2);
Z* pz = factory<Z>(2, 2);

delete pw;
delete px;
delete py;
delete pz;
}

휴..이제 Rvalue Reference와 관련된 모든 내용을 설명드렸습니다. 최대한 이해하기 쉽게 정리해서 올리려고 했는데 이것도 생각처럼 쉬운게 아니네요. ^^; 부디 많은 분들이 Rvalue Reference를 이해하는데 도움이 되셨기를 바라며 강좌를 마치도록 하겠습니다. 혹시 이해가 안되시는 부분이 있으시거나 질문이 있으신 분들은 댓글이나 메일(devmachine@naver.com)로 보내주시면 답변드리도록 하겠습니다.

Reference

MSDN - Rvalue Reference Declarator: &&