#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, bind2ndbinary_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_functionunary_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)가 넘어온다.

참고


크리에이티브 커먼즈 라이선스
Feedback plz <3 @ohyecloudy, ohyecloudy@gmail.com
|