#stl 함수 어댑터(function adaptor, adapter) - not1, bind1st, ptr_fun, mem_fun

3 minute read

함수 어댑터(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_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)가 넘어온다.

참고