C++ Boost Function 및 Bind

Table of Contents


함수 포인터

  • C++에는 다양한 종류의 포인터들이 존재한다.
  • 포인터는 당연히, 특정한 메모리의 주소를 가리킬 수 있는 변수를 의미
  • 메모리의 주소를 가리킬 수 있으므로, 다른 변수, 다른 객체, 다른 배열 등을 모두 가리킬 수 있다.
  • 이에 추가적으로, 함수도 메모리에 위치한 값이므로, 함수 또한 가리킬 수 있다.
  • 함수 포인터는 함수가 저장된 메모리를 가리키는 포인터를 의미한다.
  • 일반적으로 변수 포인터들은 변수 타입 뒤에 *을 붙이는 방식으로 포인터를 정의한다.
  • int* a;, double* b;, MyClass* c;등의 형태로 선언할 수 있다.
  • 함수도 비슷하지만, 조금 선언이 까다롭다.
  • 잘 생각해본다면, 포인터가 가지고 있는 것은 주소뿐이다.
  • 타입이 필요한 것은 해당 주소부터 얼마나 데이터를 읽어올 지에 따라 달라진다.
  • 그 말인 즉슨, 실제 저장된 주소는 모두 동일하더라도, 읽어오는 형태가 달라질 수 있다는 것을 의미한다.
  • 그렇다면… 함수를 구분해주는 것은 무엇일까??
  • 쉽게 생각한다면 Function Overloading을 생각할 수 있을 것 같다.
  • Function Overloading을 할 때, 함수들을 구분지어주는 시그니처는 함수명, 함수 입력 파라미터의 개수, 함수 입력 파라미터의 타입 세 가지가 된다.
  • 그래서 비슷한 의미로 함수 입력 파라미터의 개수 및 타입을 사용하기는 하는데, 왜 출력 타입까지 사용하는 지는… 나도 잘 모르겠다. (아시는 분 댓글 달아주세요)
  • 아무튼 함수 포인터를 구분할 때는 입력 파라미터의 개수, 입력 파라미터의 타입, 출력 타입의 3가지로 구분할 수 있다.
  • 선언은 int (*function_ptr) (int, int);와 같은 식으로 선언할 수 있다.
  • 위의 선언의 출력 타입이 int, 입력 파라미터가 int 2개를 사용하는 함수 포인터를 의미한다.
  • 아래와 같이 코드를 작성한 후, 실행해보면 결과를 볼 수 있다.
#include <iostream>

double sum(double a, double b)
{
	return a + b;
}

int main()
{
	int (*function_ptr) (int); // 입력이 int 파라미터 1개, 출력이 int인 함수
	double (*d_function_ptr) (double); // 입력이 double 파라미터 1개, 출력이 double인 함수
	double (*dd_function_ptr) (double, double); // 입력이 double 파라미터 2개, 출력이 double인 함수
	dd_function_ptr = sum; // 입력이 double 파라미터 2개, 출력이 double인 함수 sum을 저장
	std::cout << sizeof(function_ptr) << std::endl; // 함수 포인터도 마찬가지로 8바이트
	std::cout << dd_function_ptr(0.1, 0.3) << std::endl; // 이와 같은 형태로 호출 가능
	
	return 0;
}


함수 포인터를 쓰는 이유

  • 함수 포인터라는게 뭔지는 이해가 됐는데, 함수는 그냥 호출만 하면 되는 거 아닌가? 왜 굳이 포인터를 이용까지해야 하지? 라는 생각이 들 수 있습니다.
  • 하지만, 조금만 더 일반적인 경우를 생각한다면 Callback함수와 같은 형태로, 다른 함수의 파라미터로 전달할 수 있게 사용할 수 있다.
  • 저 같은 경우는 ROS에 관련된 내용을 하면서 Callback함수를 많이 다루게 되었고, 이에 따라 함수 포인터로 전달해주는 경우를 보면서 이를 정리하게 되었는데, 실상은 좀 다릅니다.
  • 사실 함수 포인터는… C언어에 있는 문법인데, 잘 사용하지 않는 것 같습니다.
  • 아니 아까 많이 쓴다며? 그게 사실 Boost가 많이 좋아지면서, 일반화된 함수 포인터를 다룰 수 있게 되었기 때문이죠.

Boost Function

  • 함수 포인터 얘기를 했는데, 일반화된(?) 함수 포인터란 뭘까요?
  • 이것은 사실 C++로 넘어오면서 생기는 문제인데, C언어에는 클래스가 없습니다.
  • 그렇기 때문에 일반 함수만 존재하며, 멤버 함수는 존재하지 않습니다.
  • 그렇다면 기존에 사용하던 함수 포인터를 이용하면 멤버 함수도 가리킬 수 있을까요?
  • 물론 있습니다. 다음과 같은 방법을 이용한다면 가능합니다.
#include <iostream>

class TestClassA
{
    public:
        TestClassA(int a, int b, int c)
        {
            this->a_ = a;
            this->b_ = b;
            this->c_ = c;
        }
        void printStatus()
        {
            std::cout << "a, b, c = " << this->a_ << ", " << this->b_ << ", " << this->c_ << std::endl;
        }

    private:
        int a_;
        int b_;
        int c_;
};

class TestClassB
{
    public:
        TestClassB(int a, int b, int c)
        {
            this->a_ = a;
            this->b_ = b;
            this->c_ = c;
        }
        void printStatus()
        {
            std::cout << "a, b, c = " << this->a_ << ", " << this->b_ << ", " << this->c_ << std::endl;
        }

    private:
        int a_;
        int b_;
        int c_;
};

int main()
{
    TestClassA A(1, 2, 3);
    TestClassB B(10, 20, 30);

    void (TestClassA::*function_ptr) () = TestClassA::printStatus;
    void (TestClassA::*function_ptr) () = TestClassB::printStatus;

    A.printStatus();
    B.printStatus();

    return 0;
}
  • 대충 보시면 아시겠지만, 클래스에 포함된 멤버 함수의 경우, 해당 클래스의 함수 포인터를 적용해야합니다.
  • 그러므로, 49번 줄의 void (TestClassA::*function_ptr) () = TestClassB::printStatus();는 컴파일 오류가 발생합니다.
  • TestClassA의 함수 포인터로 TestClassB의 함수를 가리켰기 때문이죠.
  • 내부 구현이나, 동작도 동일하고, 입력, 출력 타입도 동일한 것 같은데 안되는 게 이상하긴 합니다만, 아무튼 안됩니다.
  • 저는 사실 몰랐는데, C++Python과 마찬가지로, 멤버 함수들의 경우는 자동으로 첫 번째 파라미터로 this 포인터가 전달이 된다고 합니다. Pythonself와 마찬가지입니다만, C++은 암시적으로 전달되며, Python은 명시적으로 전달해줘야하죠.
  • 그래서 이러한 차이 때문에 입력되는 타입이 맞지 않아서 컴파일 오류가 발생한다고 보시면 될 것 같습니다.
  • 여기까지가 기존 사용하던 함수 포인터입니다.
  • 그렇다면 일반화된 함수 포인터는 어떨까요?
  • 일반화된 함수 포인터boost::function을 통해서 사용할 수 있습니다.
  • boost::function은 입력 파라미터의 개수 및 타입, 출력 타입만 맞다면 모두 적용할 수 있죠.
  • 즉, 다음과 같이 사용할 수 있다는 뜻입니다.
#include <iostream>
#include <boost/bind.hpp>
#include <boost/function.hpp>

class TestClassA
{
    public:
        TestClassA(int a, int b, int c)
        {
            this->a_ = a;
            this->b_ = b;
            this->c_ = c;
        }
        void printStatus()
        {
            std::cout << "a, b, c = " << this->a_ << ", " << this->b_ << ", " << this->c_ << std::endl;
        }

    private:
        int a_;
        int b_;
        int c_;
};

class TestClassB
{
    public:
        TestClassB(int a, int b, int c)
        {
            this->a_ = a;
            this->b_ = b;
            this->c_ = c;
        }
        void printStatus()
        {
            std::cout << "a, b, c = " << this->a_ << ", " << this->b_ << ", " << this->c_ << std::endl;
        }

    private:
        int a_;
        int b_;
        int c_;
};

int main()
{
    TestClassA A(1, 2, 3);
    TestClassB B(10, 20, 30);

    // void (TestClassA::*function_ptr) () = TestClassA::printStatus;
    // void (TestClassA::*function_ptr) () = TestClassB::printStatus;
    boost::function <void ()> function_ptr;
    function_ptr = boost::bind(&TestClassA::printStatus, A);
    function_ptr();
    
    function_ptr = boost::bind(&TestClassB::printStatus, B);
    function_ptr();
    // A.printStatus();
    // B.printStatus();

    return 0;
}
  • 위와 같이 코드를 작성하면 오류 없이, 동일한 함수 포인터 1개로 두 종류의 클래스에 있는 멤버 함수를 모두 가리켜서 사용할 수 있습니다.
  • 근데 그 중간에 보면 잘 모르는 boost::bind()라는 놈이 있습니다. 이건 뭘까요?

Boost Bind

  • Boost Bind는 쉽게 설명하면 함수를 우리가 원하는 형태로 변경할 수 있는 기능을 제공한다고 보면 됩니다.
  • 이전에 말씀드린대로, 멤버 함수는 기본적으로 this 포인터를 암시적으로 입력받고 있습니다.
  • 그렇다면 사실 TestClassB::printStatus()라는 함수는 입력 파라미터가 1개인 셈이죠, TestClassB 타입의 파라미터 1개를 받고 있습니다.
  • 그렇지만 실제 사용 시에는 암시적으로 전달되는 this 없이 사용하므로, 1개를 미리 bind 해줘야하는 것입니다.
  • 이에 따라 boost::bind를 통해, 특정 멤버 함수를 먼저 bind하고, 그 첫 번째 파라미터인 this에 해당하는 인스턴스를 넘겨주면 되는 것이죠.
  • 이 외의 사용 방법도 많이 있는데, 예를 들어 3개의 입력이 있는 함수를 1개만 입력해서 호출하고 싶다면 그럴 때도 boost::bind를 사용할 수 있습니다.
#include <iostream>
#include <boost/bind.hpp>
#include <boost/function.hpp>
void printData(int a, int b, int c)
{
    std::cout << "a = " << a << ", b = " << b << ", c = " << c << std::endl;
}

int main()
{
    boost::function<void (int)> custom_print = boost::bind(&printData, 2, 3, boost::placeholders::_1);
    custom_print(10);
    custom_print = boost::bind(&printData, boost::placeholders::_1, 2, 3);
    custom_print(10);


    return 0;
}
  • 위의 코드를 보면 입력이 3개의 int를 받으며, 출력값은 없는 함수가 있다.
  • 이를 상황에 따라 다르게 사용하기 위해 boost::bind를 사용할 수 있다.
  • boost::bind를 통해 함수를 bind하고, 그 다음 입력으로 들어가는 값을 미리 지정할 수 있다.
  • 위의 방법은 입력으로 들어가는 a에 2를, b에 3을, 그리고 c에는 입력으로 들어오는 첫 번째 파라미터를 넘겨주겠다는 의미이다. (boost::placeholders::_1은 자리 지시자로, 첫 번째 파라미터를 의미한다.)
  • 아래의 방법은 입력으로 들어가는 a에 첫 번째 파라미터를, b에 2, c에 3을 bind하는 것을 의미한다.
  • 결과를 살펴보면 a = 2, b = 3, c = 10이 먼저 출력되고 a = 10, b = 2, c = 3이 나중에 출력된다.
  • 따라서, boost::bindboost::placeholders::_1 등을 적절히 활용하면, 굳이 여러 개의 함수를 생성하지 않고 동일한 함수로 여러 가지 동작을 할 수 있다는 의미가 된다.
  • 이에 추가적인 장점으로 호환되는 시그니처를 사용하는 경우, 담을 수 있다는 장점도 있다. (int 대신 short 타입을 사용한다거나 하는 경우, 기존의 함수 포인터는 불가능)

Review

  • C++을 쓰기 전부터 Boost가 유용하다, Boost를 써본 사람보다 안써본 사람을 찾는게 더 어렵다는 얘기 등등… 많이 들었지만, 유용하긴 아주아주 유용하다.
  • 그리고 현재는 Boost에만 있는 기능이 아니라, STL로 많이 넘어왔기 때문에, C++11 이상에서는 #include functional을 선언한 후, std::function, std::bind, std::placeholders::_1을 사용할 수도 있다.
  • 아무튼 ROS관련에서 Callback함수 사용 시에 아주 많이 사용하는 Boost::bind를 알아보았다.

comments