std::bind 사용하기

Posted by LA Stranger
2014. 7. 9. 09:48 Programming/C++
 
 

std::bind 사용하기

C++11 세계로 들어오면서 전에 보도 듣도 못한 여러가지 기능들이 추가되어 매우 혼란스럽다. 배움의 길이라 생각하고 마주치는 것들 하나하나에 대해 포스팅을 할 계획이다. 우선 그 첫번째로 std::bind.

간단한 예제

아래와 같이 두개의 정수형 파라메터를 전달 받아 둘을 더하여 결과를 리턴하는 함수가 있다.

#include <iostream>

using namespace std;

int add(int a, int b) {
    return a+b;
}

int main() {
    cout << add(1, 2) << "\n";
}

컴파일 후 실행 결과는 당연하게도 다음과 같다.

3

이제 우리는 항상 위에서 정의한 add함수를 이용해 1,2를 더한 결과를 출력하는 함수를 원한다고 가정해보자.

int add12() {
    return add(1, 2);
}

위와 같은 함수를 생각해 볼 수 있을 것이다. 그리고 조금 있다 위와 비슷하게 add함수를 이용해 3,4를 더해야 하는 함수가 필요해졌다. 그러면 add34()와 같은 함수를 또 만들 수 있을 것이다. 그러나 이런식으로 매번 문제를 해결해 나가면 끝도 없이 새로 정의되는 함수가 늘어만 갈 것이다. 만약 이런 함수 정의를 매번 쓸 필요없이 필요한 경우에 위와 같은 함수를 만들어 주는 기능이 있다면 매우 편리할 것이다.
이것이 바로 std::bind 함수가 제공하는 기능이다.

std::bind 사용하기

std::bind 를 사용하기 위해서는 <functional> 표준 라이브러리 해더를 include해주어야 한다. std::bind를 이용해서 위의 문제를 어떻게 깔끔하게 처리할 수 있는지 아래의 예제를 보자.

#include <iostream>
#include <functional>

using namespace std;

int add(int a, int b) {
    return a + b;
}

int main() {
    auto f = bind(add, 1, 2);
    cout << f() << "\n";
}

위의 코드의 결과는?

3

새로운 함수를 정의해서 처리했던 일을 bind 함수로 한방에 끝냈다. 깔금하지 않은가?

bind 함수가 여기서 한 일은 첫번째 인자로 함수명을 받고 나머지 인자들을 첫번째 인자로 받은 함수의 인자에 bind해서 필요한 시점에 호출할 수 있는 객체를 만들어 준것이다. 일단 여기서 어떤 객체가 생성되었는지는 고민할 필요는 없다. 천천히 알아가도록 하자.

변수 혹은 참조 바인딩

위의 예제에서는 상수를 바인딩하는 예제를 썼는데 변수 또는 참조 변수도 바인딩 가능하다. 아래 예제를 보자.

int main() {
    int x = 1, y = 2;
    auto addxy = bind(add, x, y);
    cout << addxy() << "\n";
}

변수들의 경우 bind가 호출되는 시점의 변수값으로 bind가 일어 난다는 것에 주의를 할 필요가 있다. bind이후에 변경된 변수값은 결과에 반영되지 않는다.

int main() {
    int x = 1, y = 2;
    auto addxy = bind(add, x, y);
    cout << addxy() << "\n";
    x = 3;
    y = 4;
    cout << addxy() << "\n";
}

위 예제의 결과는 3, 7이 아니라 3, 3이다.

그러나 이것을 해결할 방법도 있다. 아래의 예제와 같이 바인딩할 때 cref라는 헬퍼 함수의 도움을 받아 참조를 넘기면 된다.

int main() {
    int x = 1, y = 2;
    auto addxy = bind(add, cref(x), cref(y));
    cout << addxy() << "\n";
    x = 3;
    y = 4;
    cout << addxy() << "\n";
}

위의 결과는 아래와 같이 이전과는 다른 결과를 보인다.

3
7

Placeholders

한국말로 해석하기가 참 애매한데. 대체자 정도로 해석을 하면 되려나? 영어뜻 그대로 어떤 위치를 임시로 잡고 있는 것을 뜻하며 나중에 교체될 대상이 된다.

만일 우리가 첫번째 인자에 1을 더해서 반환하는 add1이라는 함수가 필요하다고 가정해보자. 그런데 그 함수를 위에서 정의한 add함수를 이용해서 어떻게 구현할 수 있는지 알아보자.

#include <iostream>
#include <functional>

using namespace std;
using namespace std:placeholders;

int add(int a, int b) {
    return a + b;
}

int main() {
    auto add1 = bind(add, 1, _1);
    cout << add1(100) << "\n";
}

위의 예제의 결과는 예상했다시피 101을 출력한다.
특별한 인자 _1이 눈에 띄는데 이것이 placeholder역할을 하며 std::placeholders의 네임스페이스에 정의되어 있다. _1은 바인딩된 함수가 호출이 될 때 (위의 경우 add1 함수) 인자로 넘겨받은 인자로 대체된다. 위의 예제에서 _1은 100으로 대체가 되어 add 함수가 호출이 되고 따라서 그 결과로 101이 출력되게 되는 것이다.

_1은 첫번째 인자로 대체되며 _2는 두번째 인자로 대체되고 나머지는 _3, _4.. 이런 식으로 바인딩된 객체가 호출이 될 떄 전달 받은 인자를 원래 함수에 전달할 수 있다.

std::bind가 실제로 반환하는 것은?

C++ 표준이 정해 놓은 것은 실제로 어떤식으로 동작을 해야 한다는 것에 대한 가이드 라인이지 그것이 어떻게 구현이 되어야 하는지에 대해서는 정의해놓지 않았다. 따라서 그 표준을 구현하는 주체에 따라 달라질 수 있기 때문에 아래에서는 GCC의 구현을 살펴보도록 하자.

예를 들어 아래의 코드는

auto f = bind(add, 1, 2)

GCC 컴파일러는 bind의 결과로 아래 타입을 반환한다

std::_Bind_helper<int (&)(int, int), int, int>::type

C++11에 auto가 없었다면 이것을 일일이 다 손으로 쳐야 할뻔했다. 뭐 이런 타입을 반환하는데 첫번째 파라메터로 받는 함수의 선언 형태나 전달되는 인자의 타입에 따라 리턴되는 타입도 달라진다.

만일 입력에 따라 어떤 경우에는 1,2 를 더해야 하고 어떤 경우는 3,4를 더한 결과를 출력해야 한다면 어떻게 해야 할까? 아래와 같은 코드를 써야 할까?

int main() {
    auto f;
    int z;
    cin >> z;

    if (z % 2) 
        f = bind(add, 1, 2);
    else
        f = bind(add, 3, 4);
    cout << f() << "\n";
}

auto 키워드의 역할에 대해 잘 알고 있는 사람은 알겠지만 위의 코드는 컴파일이 안된다. auto로 f가 선언되는 시점에 컴파일러는 f가 어떤 타입인지 알아야 하기 떄문이다. 대신 아래와 같이 쓸 수 있다.

int main() {
    std::function <int(void)> f;
    int z;
    cin >> z;

    if (z % 2) 
        f = bind(add, 1, 2);
    else
        f = bind(add, 3, 4);
    cout << f() << "\n";

}

위와 같이 std::function 객체를 이용한 해결 방법이 있다. std::function에 대해서는 다음번에 다시 알아보도록 하겠다.

대체 이걸 어디에 쓰지?

여기까지 예제를 살펴보아도 당장 이것을 어디에 활용할 수 있을 것인지에 대해서는 모호하다. 일반적으로는 어떤 함수의 기능을 원하지만 잘못된 수의 파라메터나 파라메터의 타입이 맞지 않는 경우 혹은 파라메터의 위치가 잘 맞지 않는 경우 사용한다고 볼 수 있다.

예를 들어 다음의 예제를 보자.

template <typename FUNC>
string apply(const string& s, FUNC f) {
    string result;
    for (size_t i = 0; i < s.size(); i++) {
        result += f(s[i]);
    }
    return result;
}

이 함수는 문자열과 특정 함수를 파라메터로 전달 받아 함수를 문자열의 각 캐릭터를 파라메터로 하여 호출하고 결과로써 새로운 문자열을 만들어 리턴한다.

싱글 문자를 파라메터로 받는 함수에 대해서는 이것은 잘 동작한다. 예를 들어 파라메터로 전달되는 함수가 다음과 같이 다음 ASCII코드에서 다음 문자를 리턴하는 함수라면

char nextchar(char c) {
    return c + 1;
}

다음과 같이 코드를 쓸 수 있다.

string x = "foobar";
cout << apply(x, nextchar) << "\n";

그러나 우리가 만일 한개 이상의 파라메터를 받는 함수를 가지고 있다고 하자, 예를 들어 이 함수는 우리가 바꾸고 싶은 문자들의 리스트를 전달 받고 첫번째 파라메터로 전달받은 문자가 해당 문자들의 리스트에 포함이 되어 있으면 그 다음 문자를 리턴하는 함수라고 하자.

char nextif(char c, const string& chars) {
    if (char.find(c) != string::npos) {
        return c + 1;
    } else {
        return c;
    }
}

이 함수는 apply 함수에 어떻게 전달할 수 있을까?

apply(x, nextif)

혹은

apply(x, nextif, "aeiou")

둘다 불가능하다. 해답은 아래와 같이 std::bind를 이용하는 것이다!

cout << apply(x, bind(nextif, _1, "aeiou")) << "\n";

위의 경우 처럼 bind의 결과를 바로 템플릿 함수로 넘길 수 있다.

여기까지 살펴보았을 때 “뭐야. apply같은 함수 안만들면 되지!”라고 반응할 수 도 있다. 그러나 C++ 표준 라이브러리의 섹션을 보면 여기서 언급한 apply함수 같은 것들이 도처에 널려있음을 알 수 있고 따라서 std:bind의 사용은 필수적이라는 것을 알 수 있다. 그리고 또 다양한 use case를 스스로 발견해보기를 바란다.