boost::bind를 배워봅시다.

1. 멋진 예제.

C++에서 코딩을 하다보면, 다음과 같은 코드를 실행할 일이 종종 생기곤 한다.

//무식한 코드
class A
{
public:
    void Start();
    int SomeWork(int,int,int);
};

std::vector<a> v;
...(some inits)...

for(std::vector<a>::iterator itr = v.begin();
     itr != v.end(); ++itr)
{
    // itr은 포인터가 아니므로 이게 더 맞는 표현이라고 한다..
    (*itr).Start();
}

vector에 담긴 class A의 instance에 대해서, Start 메소드를 일괄적으로 호출하는 경우인데, 이런 처리를 boost라이브러리의 bind(이하 boost::bind)를 사용하면 효과적으로 처리할 수 있다. (타이핑 수가 준다)

//이쁜코드.
#include <boost/bind.hpp>
class A
{
public:
    void Start();
};

std::vector<a> v;
... (add to v) ...
std::for-each(
    v.begin(),
    v.end(),
    boost::bind(&A::Start, _1)
);

언뜻 보아도, 2개의 bracket(중괄호)와 std::vector<A>::iterator라는 긴 타입선언이 사라졌다. 키햐.

2. 인수 고정과 자리표.

boost::bind는 기본적으로 여러개의 인수를 갖고 있는 호출 가능한 C++ 객체(함수, 함수자-operator()를 구현한 struct혹은 class)의 인수들을 고정시키는 역할을 한다. 예를 들어, 인수로 a,b,c를 가진 함수를 반복적으로 호출할때, 3개의 인수중 2개를 고정시키고 하나만 바꿔가며 호출해야하는 상황에 유용한 라이브러리이다.

//간단한 예제
int x_minus_y(int x, int y)
{
    return x-y;
}
// same as x_minus_y(x,3);
x_minus_3 = boost::bind(x_minus_y, _1, 3);

위의 예제는 주어진 정수 x,y를 빼서 그 결과를 반환하는 x_minus_y라는 함수의 2번째 인자를 3으로 고정시킨 호출가능 객체(함수자)인 x_minus_3을 리턴하고 있다. 2번째 인자가 3으로 고정되었기 때문에, x_minus_3을 호출할 때는 다음과 같이 정수 하나만 넘겨주면 된다.

//호출 예제
x_minus_3(2); // 결국은 minus(2,3)과 동일한 효과를 갖는다.

x_minus_3을 생성하기 위해 사용한 구문에서 보면, _1이라는 특이한 표현을 사용하고 있는데, 이것이 자리표(placeholder)이다. 자리표는 bind에 의해 생성된 함수자가 받을 인수를 의미한다. _1은 첫번째로 받을 인수, _2는 두번째, _n은 n번째의 인수가 되겠다.

이를 응용하면 x_minus_y를 이용해, y에서 x를 뺀 결과를 넘겨주는 y_minus_x를 만들 수도 있다.

//y_minus_x
y_minus_x = boost::bind(x_minus_y, _2, _1);

위의 구문이 생성한, y_minus_x는 2번째로 받은 인수(_2)를 첫번째에 넣고, 첫번째로 받은 인수(_1)을 두번째에 넣어 x_minus_y를 호출해 준다. 이 자리표를 이용하면, 상당히 많은 응용을 할 수 있게 된다.

물론, 자리표를 사용하지 않고, 인수를 받지 않는 함수자를 생성할 수도 있다.

3_minus_2 = boost::bind(3_minus_2, 3, 2);

3_minus_2의 호출 결과는 언제나 1이 될 것이다.

3. 멤버함수와 bind

멤버함수를 bind와 사용하기 위해서는 boost::mem_fn이란 라이브러리에 대한 이해가 필요하다. 간단히 예제를 통해 알아보면,

//1절에서 사용한 class A를 그대로 쓰겠습니다아-
a_somework_functor = boost::mem_fn(&A::SomeWork);
A a_instance;
// same as a_instance.SomeWork(1,2,3);
a_somework_functor(a_instance, 1, 2, 3);

위와 같은 구문에 의해 생성된 a_start_functor함수자는 첫번째 인수로 주어지는 A의 인스턴스를 이용해 SomeWork을 호출하게 된다. C++에서 인스턴스와 멤버함수의 포인터를 이용해 함수를 호출하는 정식 구문이 없고, 이를 구현하기 위해서는 operator .*나 operator .->를 활용해야 하는데, 이 작업이 생각보다 복잡하므로, 이러한 함수자의 형태로 편리하게 이용하는 것이다. (자세한 사항은 Modern C++ Design 5장 참고)

1장에서 사용했던 예제를 다시 가져와서 살펴보자.

std::for-each(v.begin(), v.end(), boost::bind(&A::Start, _1));

boost::bind가 사용자의 편의성을 고려해주지 않았다면 boost::bind(&A::Start, _1) 대신 boost::bind(boost::mem_fn(&A::Start), _1)을 사용했어야 할 것이다. 하지만, 영리한 boost::bind는 첫번째 인수로 멤버함수가 올 경우 알아서 boost::mem_fn을 이용해 처리를 해준다.

boost::bind(&A::Start, _1)에 의해 생성된 함수자는 첫번째 인수로 A의 인스턴스를 넣어주어야 한다. 즉, boost::bind(&A::Start, _1)(a)는 a.Start()와 같은 의미를 갖게 되는 것이다.

std::for-each 구문은 첫번째와 2번째 인수로 주어진 iterator의 범위 내에서 각각의 요소에 대한 참조를 3번째 인수로 주어진 함수자를 이용해 호출한다. (STL 관련 서적 참고) 따라서 v에 들어있는 모든 a의 인스턴스에 대해 멤버함수인 A::Start()를 호출하게 된다.

4. 합성 함수.
수학 시간을 되돌려 보자. f(g(x))라는 수식이 기억 나는가? 그렇다. g(x)의 결과를 이용해 f(x)에 대입시켜 그 결과를 사용하는 합성함수이다. boost를 사용하면 C++에서 합성함수를 쉽게 만들 수 있다.

//합성함수예제
int f(int x)
{
    return x+3;
}

int g(int y)
{
    return (-1)*y;
}

g_f_x = boost::bind(g, boost::bind(f, _1)); // same as g(f(x))
f_g_x = boost::bind(f, boost::bind(g _1)); // same as f(g(x))

g_f_x 함수자를 만들때 사용된 bind의 예를 보면, g의 첫번째 인수를 boost::bind(f, _1)로 주고 있다. 이는 boost::bind(f, _1)의 결과를 첫번째 인수로 주겠다는 의미가 되며, 이는 우리가 원하던 합성함수이다!

합성함수를 잘 이용하면 세상이 편해진다.

struct Item{
   int score();
   Summary get_summary();
};

struct Summary
{
    bool check();
};

std::vector<item>; v;

v에 들어있는 Item중 get_summary().check()의 결과가 true가 되는 Item의 인스턴스를 찾는 문제를 생각해보자. 이런 경우에 bind가 또 한번 괴력을 발휘한다.

//합성함수를 이용한 찾기 예제.
std::find_if(
    v.begin(),
    v.end(),
    boost::bind(
        &Summary::check,
        boost::bind(Item::get_summary, _1)
    )
);

여러 줄로 나눠서 썼지만, 실은 한줄이다! get_summary(_1).check()를 bind를 이용해 표현하면 이렇게 되는 것이다.

5. 정렬.

boost 1.33에 업데이트된 bind는 <, >, ==와 같은 기초 비교 연산자를 지원한다. 이게 무슨 소리인가 싶지만, 예제를 보면 감이 올 것이다.

std::sort(
    v.begin(),
    v.end(),
    boost::bind(&Item::value, _1) < boost::bind(&Item::value, _2)
);
&#91;/cpp&#93;
v에 들어있는 Item들을 Item::value의 결과값을 이용해 정렬하는 예제이다. 만세! bind에 의해 생성된 함수자에 operator<를 오버로딩 하여, <가 있을 경우 비교하는 함수자를 자동으로
생성해 주는 기능! 현재, ==, !=, <=, >=, <, > 를 지원한다고 하니 매우 편리할 듯 싶다.

좀 더 알아보면,

[cpp]
std::find_if(
    v.begin(),
    v.end(),
    boost::bind(&Item::value, _1) == 3);

위와 같이 Item::value()의 결과값이 3인 요소를 찾는 것도 쉽게 가능하다.

6. 결론

클래스와 클래스들의 컨테이너, 그리고 STL의 강력한 알고리즘을 사용하는데 상당한 애로사항이 존재했던 것이 사실이다. 매번 함수자를 만들어 작업해야 했던 옛날과 비교하면 행복할 따름이다. boost::bind는 STL의 진정한 강력함을 느끼기 위해 필수적으로 사용해야 하는 라이브러리라고 생각된다. (다행히 STL확장과 관련된 Draft에 보면 이런 형태의 bind가 기본으로 들어올 듯 싶다)

또한, bind와 함수자를 적절히 잘 이용한다면, 콜백과 같은 기능을 구현하는데 매우 유용할 수 있다. 또, Command패턴을 이용하는데도 매우 유연하게 적용될 수 있다.

A. boost::function
예제에서 사용한 boost::bind의 결과물 들은 타입을 지정하지 않은 채 변수에 대입되고 있다. 귀찮아서 생략한 것이고, 실제로는 타입을 지정해줘야 하는데, 그 타입은 boost::function을 사용하면 쉽게 할 수 있다. boost::function은 함수자를 정의하는 라이브러리라고 생각해도 무방하며, 선언된 변수에는 signature가 맞는 어떤 호출가능한 객체라도 넣을 수 있으니 참고하시길.

쓰는 법은 다음과 같다.

// 리턴값이 없고, 인수가 int 하나인 함수 f1
boost::function<void (int)> f1;
boost::function<int (double, std::string&)> f2;

// int를 리턴하고, double과 std::string의 참조를 받는 함수 f2

다만, VC6와 같은 오래된 컴파일러에서는 구문이 다소 달라질 수 있으므로 참고하시길.

B. 참조
boost에서 함수자와 관련된 라이브러리를 사용하다보면, 종종 위험한 상황이 종종 발생한다.

std::string a = "AAAAAA";
// a의 복사본이 전달된다!
boost::bind(some_function, a);
// a의 레퍼런스가 전달된다!
boost::bind(some_function, boost::ref(a));

C++의 특성상 발생하는 문제라고 보여지는데, bind 템플릿을 전개할때, 참조로 호출해야하는 상황에 복사본이 전달되는 상황이 발생할 수 있다. 이를 해결하기 위해, boost::ref가 존재한다. 그냥 쉽게 boost::ref를 안쓰면 전부 call-by-value이고, 쓰면 call-by-reference라고 생각하시면 되겠다.

C. References

boost의 함수관련 문서들
Modern C++ Design, Andrei Alexandrecu, Addison Wesly (이기형 역, 인포북 발간)

ps. 피드백 부탁 드립니다.
ps2. 불펌 금지입니다.
ps3. 워드프레스로 옮겼더니 깨지더군요. 그래서 수정.
ps4. 멤버함수 관련된 부분이 수정되었습니다. A::Start라고 쓰면, 표준에 어긋나더군요. &를 붙여서 함수주소를 넘기는 방법이 정확한 방식입니다.

글쓴이: crowmania

Chief Developer in Somansa Guitar/Vocal in Tonics Member of Justice Party

“boost::bind를 배워봅시다.”의 9개의 생각

  1. 좋은 정보 감사합니다 ^.^ 2주동안 고민했던게 정확히 여기 있었네요~

  2. 좋은 자료 감사합니다.
    bind가 궁금해서 찾아보다가 2017년도 11월인데 덧글 남기네요.

    그런데 boost::bind는 std::bind로 c++11에서 포함된게 아닌가요?
    참고하신 책이 예전꺼라 boost에 있는거죠?

    2014년에 올린 글이신데 std::bind가 아니고 boost::bind인게 궁금해서 덧글 남깁니다.
    이해하기 쉽게 써주셔서 많은 도움이 되었습니다!

    1. 음 일단 이 글은 저도 놀랐는데 2005년에 쓴 글이네요. 2014년은 아마 워드프레스로 옮긴 시점일겁니다. boost::bind가 std::bind로 표준화된건 말씀하신 대로입니다. C++11을 지원하는 환경이면 std::bind쓰는게 더 좋을거고, 그게 아니라면 boost::bind 쓰는게 더 좋을거고, 귀찮다면 그냥 boost::bind 쓰는거죠.

      컴파일러 교체에 따른 부담을 생각하면 boost::bind를 고수하는 것도 나쁘지는 않아 보입니다. (사실 귀찮아서 안바꿉니다만..)

  3. class A
    public:
    void Start();

    boost::bind(&A::Start, _1)

    본문 두 번째 예제 코드에서 ‘_1’ 이해가 안됩니다.
    bind 에서 _n은 매개변수에 자릿수라고 했는데 start 함수는 매개변수가
    없는데 ‘_1’이 무얼 가리키는지 모르겠습니다.
    혹시 오타가 아닌가 해서 댓글 남깁니다.
    오타가 아니라면 부족한 저에게 설명 답글 부탁 드립니다.

    1. n은 1보다 큰 정수입니다. 그러니 _n은 사실 _1, _2, _3 같은거죠.
      메일함 정리를 해야겠네요. 너무 늦은 답변이 아닌지.. 미안합니다.

crowmania에 답글 남기기 응답 취소

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다