#stl 함수 어댑터(function adaptor, adapter) - not1, bind1st, ptr_fun, mem_fun
함수 어댑터(function adaptor)?!
bool IsOdd(int num)
{
return num % 2 == 1;
}
int main()
{
int numArr[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
typedef std::vector IntegerVector;
IntegerVector v(numArr, numArr + sizeof(numArr) / sizeof(numArr[0]));
IntegerVector::iterator iter = std::find_if(v.begin(), v.end(), IsOdd);
if (iter != v.end())
{
std::cout <<
"std::find_if(v.begin(), v.end(), IsOdd) : " << *iter << std::endl;
}
return 0;
}
이런 코드로 컨테이너에 있는 원소 중 첫 번째 홀수를 찾을 수 있다. 만약 첫 번째 짝수를 찾으려고 한다면? IsEven()를 만들어도 되는데, 함수 어댑터를 사용하면 not1(IsOdd)로 기존 코드를 재사용 할 수 있다. (물론 바로 어댑터를 쓸 수 있는 건 아니고 어댑터를 붙일 수 있게 IsOdd()
를 수정해야 한다.) 이처럼 함수 어댑터는 기존 코드의 재사용을 도와줘서 생산성을 향상시킬 수 있게 한다.
어댑터 적용이 가능(adaptable)하게
struct IsOdd : public std::unary_function<int, bool>
{
bool operator()(int num) const { return num % 2 == 1; }
};
적용 가능하게 하는 방법은 간단하다. 단항이면 unary_function, 이항이면 binary_function을 상속받아서 함수 객체(function object)로 만들면 된다. 템플릿 매개변수에 인자 타입과 리턴 타입을 넣어주면 된다.
이렇게 상속받아야 하는 이유는 함수 어댑터가 인자로 들어온 함수 객체에 정의된 타입을 참조하기 때문인데, not1
의 구현을 잠시 살펴본다면,
template<class _Fn1> inline
unary_negate<_Fn1> not1(const _Fn1& _Func)
{ // return a unary_negate functor adapter
return (std::unary_negate<_Fn1>(_Func));
}
인자로 들어온 함수 객체 타입으로 unary_negate
함수 객체를 만들어 리턴하게 되는데,
template<class _Fn1>
class unary_negate
: public unary_function<typename _Fn1::argument_type, bool>
{ // functor adapter !_Func(left)
public:
explicit unary_negate(const _Fn1& _Func)
: _Functor(_Func)
{ // construct from functor
}
unary_negate
클래스가 상속받을 unary_function
의 타입 인자로 함수 어댑터의 argument_type
타입이 쓰인다. 함수 어댑터도 unary_function
또는 binary_function
를 상속받는데, 이렇게 구현해서 함수 어댑터를 연달아서 호출도 가능하다. std::not1(std::not1(IsOdd()))
와 같이 어댑터를 여러 개 붙여 쓸 수 있다는 얘기.
not1, not2, bind1st, bind2nd
not1, not2 는 리턴 값에 not 연산을 하는 함수 어댑터로써 리턴 타입이 bool인 함수 객체에만 사용할 수 있다. 뒤에 숫자는 함수 객체의 operator() 연산자가 받는 인자 개수로 각각 unary_function
, binary_function
을 상속받은 함수 객체에 쓸 수 있다.
bind1st, bind2nd 는 binary_function을 unary_function으로 바꾸어 주는 함수 어댑터인데, 첫 번째 혹은 두 번째 인자에 특정한 값을 넣을지가 두 함수 어댑터의 차이점이다. 물론 앞에서 설명했듯이 binary_function
에만 사용 가능하다.
STL에는 많이 쓸 것 같은 놈들을 미리 정의해 놓았는데, 그 중 greater는 arg1이 arg2보다 크면 true인 binary_function
이다. 이 녀석을 예로 들면,
std::binder1st< std::greater<int> > greater_5_x
= std::bind1st(std::greater<int>(), 5);
IntegerVector::iterator iter =
std::find_if(
v.begin(),
v.end(),
greater_5_x);
이 경우는 arg1에 5를 넣었으므로 결과적으로 컨테이너에서 5보다 작은 첫 번째 원소를 찾게 된다.
std::binder2nd< std::greater<int> > greater_x_5
= std::bind2nd(std::greater<int>(), 5);
IntegerVector::iterator iter =
std::find_if(
v.begin(),
v.end(),
greater_x_5);
반대로 arg2에 5를 넣었으므로 컨테이너에서 5보다 큰 첫 번째 원소를 찾는다.
IntegerVector::iterator iter =
std::find_if(
v.begin(),
v.end(),
std::bind1st(std::greater<int>(), 5));
std::binder1st
는 위에서 설명했듯이 binary_function
을 unary_function
으로 바꿔주는 bind1st
가 반환하는 unary_function
이다. 따로 정의하지 않고 바로 써도 상관없는데, 나중에 함수 호출의 깊이가 깊어질 경우 가독성을 위해 따로 빼주는 게 좋다. 이 경우는 간단하니깐 바로 집어 넣는 게 좋다. 지나치게 많은 정의는 더러운 네이밍의 원인이 되기도 하니깐~
ptr_fun
함수 어댑터를 좀 써보려고 했더만 unary_function
이나 binary_function
을 상속받아야 하고 간단한 동작인데, 이 짓하기 너무 귀찮을 때 유용하게 사용할 수 있다. 컴파일러가 템플릿 인자를 추론하는 과정에서 함수 어댑터에서 사용하는 타입들이 정의된다. 징검다리 역할을 해주는 함수 어댑터.
bool IsOdd(int num)
{
return num % 2 == 1;
}
IntegerVector::iterator iter =
std::find_if(
v.begin(),
v.end(),
std::not1(std::ptr_fun(IsOdd)));
IsOdd
함수에 바로 not1
함수 어댑터를 붙이지 못하지만 ptr_fun
을 사용하면 붙일 수 있게 된다.
mem_fun, mem_fun_ref
mem_fun, mem_fun_ref 멤버 함수를 호출할 때 유용하게 사용할 수 있는 함수 어댑터이다. operator ->
로 멤버 함수를 호출하느냐 operator .
로 멤버 함수를 호출하느냐가 이 둘의 차이점이다.
class UiComponent
{
public:
void Render()
{
std::cout << "UI Component Rendering" << std::endl;
}
};
typedef std::vector<UiComponent*> UiComponentPtrVector;
UiComponentPtrVector v;
...
std::for_each(
v.begin(),
v.end(),
std::mem_fun(&UiComponent::Render));
typedef std::vector<UiComponent> UiComponentVector;
UiComponentVector v;
...
std::for_each(
v.begin(),
v.end(),
std::mem_fun_ref(&UiComponent::Render));
“mem_fun
이고 mem_fun_ref
고 다 필요 없어!”라면
void Render(UiComponent* p)
{
p->Render();
}
std::for_each(v.begin(), v.end(), Render);
비멤버 함수를 하나 정의해서 사용해 똑같은 일을 시킬 수도 있다.
대충 동작 방식이 상상은 가지만 한번 코드를 살펴 본다면,
template<class _Result, class _Ty>
mem_fun_t<_Result, _Ty> mem_fun(_Result (_Ty::*_Pm)())
{ // return a mem_fun_t functor adapter
return (std::mem_fun_t<_Result, _Ty>(_Pm));
}
template<class _Result, class _Ty>
class mem_fun_t : public unary_function<_Ty *, _Result>
{ // functor adapter (*p->*pfunc)(), non-const *pfunc
public:
explicit mem_fun_t(_Result (_Ty::*_Pm)())
: _Pmemfun(_Pm)
{ // construct from pointer
}
_Result operator()(_Ty *_Pleft) const
{ // call function
return (_Pleft->*_Pmemfun)();
}
private:
_Result (_Ty::*_Pmemfun)(); // the member function pointer
};
멤버 함수 포인터(_Pmemfun
)를 들고 있다가 operator()
함수가 호출될 때 넘어온 인자를 이용해 호출한다. mem_fun_ref_t
같은 경우는 operator()
의 인자로 참조(_Ty& _Pleft
)가 넘어온다.
참고
- TC++PL
- 이펙티브 STL