: 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
따라서 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
lvalue 가 오면 static_cast
하지만 좀 더 직관적으로 표현할 수는 없을까?
캐스팅이 아닌 함수를 호출하는 것으로 대신할 수는 없을까?
캐스팅하는 코드를 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 을 사용하여 이 단점을 해결할 수 있다.
댓글 없음:
댓글 쓰기