2015년 9월 14일 월요일

[C++11] Perfect forwarding

Perfect forwarding
: wrapper function(or class)를 만들 때 인자를 완벽하게 전달하는 기술


위 예제는 wrapFunc()함수에 함수와 인자를 전달하여 전달한 함수에 인자를 완벽하게 전달해보려 시도?하는 예제이다.
물론 전혀 완벽하지 않은 코드이다.


template<typename F, typename T> void wrapFunc(F f, T a) 함수는
a를 값으로 전달 받기 때문에 참조가 아니게 되어 n의 값이 변경되지 않는다.

그렇다면 a를 참조로 받는다면 되지 않을까?

하지만 이 함수의 문제점은 rvalue 의 reference 를 받을 수 없다.
T& a 는 rvalue reference(위 예제에선 wrapFunc(foo, 10); 호출 시 10이 rvalue)가
전달되는 경우를 처리할 수 없다.

그렇다면 rvalue 를 전달받기 위해 const 키워드를 이용한다면 가능하지 않을까?

하지만 역시 const 키워드를 사용하면 goo(int&) 함수를 호출하는데 문제가 된다.(signature가 맞지 않음)

결국 위 예제는 perfect forwarding 이 되지 않는다.
이제 이 문제를 해결해보자!!



해결책 1)
함수 overloading 을 이용해서 const 인자가 있는 것과 없는 function 을 2개 선언한다.

단점 : N개의 인자를 perfect forwarding 시키려면 2^n 개수로 template function을 만들어야 한다.

다른 해결 방법을 보기 전에 참조(Reference)에 대해 잠시 생각해보자.

r1 = r2; 코드는 값이 복사되는 것일까? 참조하는 주소가 복사되는 것일까?
정답은, 값이 복사된다!!

Why?
C++에서 참조라는 개념은, 한번 초기화되면 다시는 다른 곳을 가리킬(참조) 수 없다.
const 와 동일한 느낌이다.
그러므로 r1 = r2; code 에서 r1이 가리키는 참조 주소가 변경되는 것이 아니라(변경될 수 없음),
r2가 참조하는 곳의 값이 r1이 참조하는 곳에 다시 쓰여지는 것이다.

참조와 포인터는 모두 기존 메모리를 가리키게 된다.
하지만 대입 연산시에 차이점이 있다.
참조(Reference) : 값이 이동한다. 값을 꺼낼 때 *를 붙일 필요가 없다. 참조는 자동으로 역참조가 되는 상수 포인터라고 볼 수 있다.
포인터(Pointer) : 참조(주소)가 이동한다. 값을 꺼낼 때 *를 붙여야 한다.

그렇다면 값이 아닌 참조가 이동할 수 있는 참조를 만들 수 있지 않을까?!


해결책 2) 이동 가능한 참조 만들기

C++의 참조는 기존 변수에 이름을 하나 만들어서 메모리 주소를 가리키는데 사용한다.
결국에는 객체 하나 만들어서 그 안에 포인터 두고 그 주소를 가리키게끔 하는 거랑 똑같다.
우리는 그것을 xreference_wrapper class로 만들었다.
그리고 참조는 다른 변수를 가리켜야 한다. 그러기 위해선 포인터가 필요하고..
그 포인터는 T* obj; 이다.

main() 함수에서 r1 = r2; 코드는 값이 아닌 참조가 이동한다.
우리는 xreference_wrapper 클래스에 대입연산자를 정의하지 않았지만,
default 대입 연산자가 동작할 것이고 이것은 포인터가 가리키는 주소를 변경하게 된다.
그렇다면, *(asterisk)없이 값에 접근할 수 있는 code만 추가되면 된다.
그것은 &연산자를 재정의함으로써 가능하다.

또한 &연산자를 재정의함으로써 int& r3 = r1; 코드에서
r1은 객체인데, c++에서 변환 연산자가 동작하므로 결국엔 int* 를 r3에 전달할 수 있게 된다.

Reference(참조)를 class 로 구현해서 값이 아닌 참조가 이동되도록 했다.


xreference_wrapper 를 이용해서 wrapFunc함수를 호출해보자.

위 예제에서

코드는 모두 n의 주소를 보낸다.
r은 은 xrefernce_wrapper 이므로 int& 으로 변환이 가능하다.
따라서 wrapFunc에 전달된 T 는 int& 가 된다.

하지만 &n 은 wrapFunc에 전달된 T가 int* 가 되므로 goo(int&)으로 호출이 불가능하다!!
여전히 문제점이 남아있다.



먼저 이전 예제에서 사용한 xreference_wrapper에 대해 생각해보자.
Class template(클래스 템플릿)은 T의 암시적 추론이 불가능해서 항상 어렵다.
그렇다면 암시적 추론이 가능한 Function template(함수 템플릿)을 사용해 좀 더 쉽게 만들어 보자!

xref 함수는 전달된 T& 를 이용해서 참조를 나타낼 수 있는 xreference_wrapper로 변환하여 반환하는 함수이다.
xreference_wrapper 로 반환될 경우 참조를 전달받는 함수에 전달이 가능하다!


r은 main 함수 내에 계속 쓸것인가? 아니다. wrapFunc에 잠깐 전달하기 위한 용도다.
그러므로 임시객체를 사용하는 것이 더 바람직하다.

하지만 xreference_wrapper 는 class template 이기 때문에, 생성자를 통해서 type을 추론할 수 없다.
그래서 를 항상 써야 한다.
하지만 function template(함수 템플릿)은 전달되는 인자로 type이 추론 가능하다!



결국엔 참조를 보내려면 이렇게 해야 한다.
Function template은 전달된 인자의 타입을 추론할 수 있어서
함수 내부에서 다시 Class template으로 전달할 때는 이미 추론한 type을 전달하기 때문에
Class template에 전달하는데 문제가 없게 된다.

참조를 전달할 수 있는 ref 를 사용하는 예제를 살펴보자.

먼저 C style의 코드부터 살펴보자.

f1 함수 포인터는 인자가 없고 반환도 하지 않는 함수를 가리킬 수 있다.
위 예제에서는 foo 함수를 가리키는데,
f1 함수 포인터는 signature 가 다른 함수를 가리킬 수 없다.

그럼 C++ 에서는 어떻게 할 수 있을까?

f 의 장점은 signature가 다른 goo 함수를 가리키게 할 수도 있다는 것이다.

하지만 goo 함수를 가리키도록 하여 goo함수를 호출하면,
n 의 값이 변경되지 않음을 알 수 있다.

perfect forwarding 이 되지 않았다는 의미이다.



결국엔 ref 를 이용하여 참조가 전달되도록 하면 n 이 변경되는 것을 확인 할 수 있다.



rvalue, lvalue에 대한 예제를 살펴보자.

foo(int&)에 전달된 c 는 const int 이므로 int& 로 전달받을 수 없다.
또한 goo(int)에 전달된 n 은 int& n으로 전달되었기에 error 가 발생한다.

그렇다면 functcion template 으로 바꾼 후에는 어떻게 될까?

foo(c); 을 호출하면 T가 const int 로 결정되므로 문제가 없다.
또한 goo(&n); 을 호출하면 T가 int* 가 되므로 문제가 없다.

이제 &가 하나가 아닌 좀 더 복잡한 경우를 살펴보자.

이전 포스트에서 &은 lvalue reference 이고, && 은 rvalue reference 라고 했었다.
foo(int&&) 함수는 rvalue reference 를 인자로 받는다.
따라서 foo(10) 은 동작하지만, n은 lvalue 이기 때문에 foo(n) 호출 시 문제가 된다.

goo(T&&)함수를 살펴보자.
goo(r) 을 호출할 때 T는 어떻게 될까?
r은 int& 타입인데, 결국에 goo 에 전달된 a 는 int& && 타입을 갖는 것일까?
이렇게 &가 3개 이상 충돌할 경우에는 어떤 reference 타입이 되는지 규칙이 있다.

& && 만날 경우 & 가 되고
&& & 경우 & 가 되며,
&& && 는 && 가 된다.

결국 && && 두개가 만나야 && 가 된다.

이 규칙에 따라서 goo(r)를 호출하면 결국 a 는 int&& 타입이 된다.

정리하자면,
int& : lvalue reference 는 lvalue 타입만 받는다.
int&& : rvalue reference 는 rvalue 타입만 받는다.
T&& : universal reference(forward reference) 라고 부른다.
이 Universal reference 는 lvalue 가 전달되면 lvalue reference 가 되고
(T : int& 되므로, T&& int& && 가 되고 => int &)
rvalue 가 전달되면 rvalue reference 가 된다.
(T : int&& 되므로, T&& 는 int&& && -> int&&)


지금까지 얘기하는 이 모든게 perfect forwarding 을 위해서이다.
타입의 종류와 인자 개수에 제한받지 않고 인자를 전달받기 위해서 template 과 universal reference 를 쓰는 것이다.

그렇다면 universal reference 를 사용하는 간단한 예제를 살펴보자.

wrapFunc(foo, 10); 을 호출하면 10은 rvalue 이므로,
T&& 는 int&& 가 되어 rvalue 를 전달받을 수 있게 되고 foo(int) 함수를 호출하는데 문제가 없다.
또한 wrapFunc(goo, n); 을 호출하면 n 은 lvalue 이므로,
T&& 는 int&가 되어 lvalue 를 전달받을 수 있게 되어 goo(int&) 함수를 호출할 수 있다.



위 예제에서 문제점은 무엇일까?

wrapFunc(foo, 10)을 호출하면 10은 rvalue 이기 때문에 T&& 는 int&& 로 전달된다.
하지만 a는 lvalue 이기 때문에! foo(int&&) 에 전달할 수 없다.

a가 lvalue 인 것이 이해하기 어렵다면 아래 코드를 살펴보자.

위 코드에서 r 은 rvalue 인가? lvalue 인가?
10은 rvalue 이지만 r 은 lvalue 이다.
이전 예제에서도 동일하게 wrapFunc(F f, int&& a) 에서 a는 lvalue 가 되는 것이다.
그리고 이 lvalue 인 a를 foo(int&&)에 전달하니 error가 발생하는 것이다.

그렇다면 casting 을 해서 rvalue 일 때는 rvalue 로, lvalue 라면 lvalue로 전달해주면 되지 않을까?

static_cast<T&&> 는 인자가 rvalue 라면 rvalue 로, lvalue 라면 lvalue로 캐스팅해준다.
rvalue가 오면 static_cast(a);
lvalue 가 오면 static_cast(a); 가 된다.

하지만 좀 더 직관적으로 표현할 수는 없을까?
캐스팅이 아닌 함수를 호출하는 것으로 대신할 수는 없을까?

캐스팅하는 코드를 xforward란 함수를 만들어 감추고 단순 함수 호출로 사용해보자.
하지만 이 코드에는 문제가 있다.
wrapFunc(F f, T&& a)에서 a로 10을 전달하면,
T&& 는 int&&가 되고 a는 lvalue 가 된다.
따라서 xforward의 T&& 는 int& 로 추론된다.
컴파일러가 타입을 암시적으로 추론해버리는 것이다.

결국엔 반드시 컴파일러가 명시적으로 추론하도록 해야 한다.

위 예제에서 만든 xforward는 이미 C++ 표준에서 forward 함수로 제공하고 있다.

그렇다면 이제 타입에 관해서는 해결을 했으니, 여러 개의 인자를 받고 반환해주는 일이 남았다.

일단 여러개의 인자를 전달 받기 위해서는 '...' 를 인자에 붙여준다.
또한 forward 에 전달할 때도 ... 를 붙여주는데 (args) 밖에 써야한다는 점에 주의하자.

return type을 알 수 없으므로, auto 키워드를 사용하여 반환한다.

완벽한 전달자가 되려면,
1. 인자를 Universal(forward) reference 사용
2. 원래 함수를 보낼 때 인자를 다시 forward로 묶어서 전달
3. forward 방식의 명시적 추론을 사용
4. 여러개의 인자를 받기 위해 가변인자 템플릿을 사용

하지만 아직까지도 문제점이 남아 있는데...
반환 타입이 참조일 경우 문제가 생긴다.
만약 int& foo(int a); 라면 어떻게 될까?


그전에 잠시 ... 가 밖에 써야하는 점에 대해 간단한 예제를 통해 살펴보자.

goo(args...);를 호출하면 goo(1, 2, 3); 이 될 것이다.
hoo(args...); 를 호출하면 어떻게 될까?
에러가 발생한다. hoo 함수는 인자가 1개이기 때문이다.

goo(hoo(args)...); 를 호출하면
goo(hoo(args의 1번째), hoo(args의 2번째), hoo(args의 3번째)); 와 같다.

이전 예제에서 ... 를 밖에 써야 하는 이유에 대해 조금은 이해가 됐으리라고 생각한다.

마지막으로 Perfect forwarding 이 적용된 예제를 살펴보자.

Android 에서 singleton 을 구현할 때 사용하는 CRTP 기법에 perfect forwarding 을 적용해보자.

CRTP 기법은 부모가 템플릿인데 자식이 상속 받으면서 자신의 이름을 부모에 템플릿 인자로 전달하는 기법이다.
하지만 반드시 default constructor가 필요하다는 단점이 있다.
perfect forwarding 을 사용하여 이 단점을 해결할 수 있다.







댓글 없음:

댓글 쓰기

[C++] meta programing

재귀 호출에 관해 template meta programming 을 적용한 예제를 살펴보자. #include using namespace std; int fact(int n){ if(n factorial 연산을 하는 일반적인 재귀 호출 함...