Inter-thread communication library in C++ v0.1

Posted at 2008. 11. 12. 23:05 // in S/W개발/C++ 이야기 // by 김윤수


Inter-thread communication library에 signal 기능까지 구현해서 이제 어느 정도 모양새를 갖춘 것 같습니다. 그래서 버전을 한 번 붙여 보았지요. ^^ 지난 번과 비교해서 signal 구현이 가장 크게 바뀐 부분이니 그 부분을 중심으로 말씀드리겠습니다.

signal 클래스의 이름은 ITCSignal0, ITCSignal1, ITCSignal2, ITCSignal3 입니다. 각각 인자가 0개, 1개, 2개, 3개가 있는 slot을 연결 할 때 사용합니다. Call Trait를 분해하고 합성하는 방법에 익숙하면 boost::signal 이나 boost::function 처럼 ITCSignal 클래스 하나만 정의하고, 함수 Signature 전체를 type으로 받아들이는 식으로 할 수 있을텐데 아직 내공이 부족하여 거기까진 못하겠더군요. 물론 천천히 공부하면서 할 수도 있지만 워낙 성격이 급하고 빨리 돌아가는 것도 보고 싶은지라 그냥 익숙한 방식대로 했습니다.

ITCSignal0는 어차피 필요한 타입 파라미터가 없기 때문에 템플릿 클래스가 아닌 그냥 일반 클래스로 정의했구요, ITCSignal1, ITCSignal2, ITCSignal3는 각각 인자의 타입 정보가 필요하므로 템플릿 클래스로 정의했습니다. 서로 다른 클래스이지만 모두 동일한 멤버 함수를 갖습니다. 먼저 가장 기본이 되는 ITCSignal0부터 살펴 보겠습니다.

class ITCSignal0
{
public:
    typedef boost::function<void ()> SlotType;
    typedef const SlotType& ConnectionType;

    ITCSignal0() : m_vec() {}

    template <typename C>
    ConnectionType connect(C* inst, void (C::*call)())
    {
        return connect(boost::bind(call, inst));
    }

    template <typename Caller, typename Callee = Caller>
    struct DoIt
    {
        typedef void (Callee::*CallType)();
        typedef void (Caller::*Type)(CallType);
    };

    template <typename Caller, typename Callee>
    ConnectionType connect(Caller* inst,
                           typename DoIt<Caller, Callee>::CallType call)
    {
        return connect(boost::bind(typename DoIt<Caller,Callee>::Type(&Caller::doIt), inst, call));
    }

    template <typename C>
    struct FuncDoIt
    {
        typedef void (*CallType)();
        typedef void (C::*Type)(CallType);
    };

    template <typename C>
    ConnectionType connect(C* inst, typename FuncDoIt<C>::CallType call)
    {
        return connect(boost::bind(typename FuncDoIt<C>::Type(&C::doIt), inst, call));
    }

    ConnectionType connect(ConnectionType call)
    {
        m_vec.push_back(call);
        return m_vec[m_vec.size()-1];
    }

    void disconnect(ConnectionType call)
    {
        std::vector<SlotType>::iterator it = m_vec.begin();
        for (;it != m_vec.end(); ++it)
        {
            if (&(*it) == &call)
                break;
        }
        if (it != m_vec.end())
        {
            m_vec.erase(it);
        }
    }

    void operator()()
    {
        std::for_each(m_vec.begin(), m_vec.end(),
                      boost::mem_fn(&SlotType::operator()));
    }

private:
    std::vector<SlotType> m_vec;
};

보시다시피 slot을 저장해두는 storage로는 그냥 SlotType(boost::function<void ()> type)의 vector를 사용하고 있습니다. 그냥 vector를 이용해서 connect(ConnectionType call)과 disconnect(ConnectionType call)을 구현하고 있습니다. 그런데, disconnect() 같은 경우, connect()의 리턴값을 기억하고 있다가 disconnect()를 호출하면 되니까 별 문제가 되지 않지만, connect(ConnectionType) 같은 경우 ConnectionType의 객체 인스턴스를 구성하는 것 자체가 무척 귀찮은 일입니다. boost::bind를 쓴다고 하더라도 상당한 수작업으로 인해 컴파일 에러가 많이 발생할 소지가 있습니다. 예를 들어, 다음과 같은 클래스가 선언되어 있는 상황에서

struct Base
{
    virtual void print()
    {
        cout << "Base::print()" << endl;
    }
};

struct Derived : public Base
{
    virtual void print()
    {
        cout << "Derived::print()" << endl;
    }
};

struct BaseITCAdaptor : public ITCAdaptor<Base>
{
    BaseITCAdaptor(shared_ptr<Base> b, const string& name) :
        ITCAdaptor<Base>(b, name)
    {}

    void print()
    {
        doIt(&Base::print);
    }
};

특정 BaseITCAdaptor 인스턴스에게 &Base::print 멤버 함수를 실행하게 하고 싶으면 다음과 같이 해야 합니다.

BaseITCAdaptor* adpt = new BaseITCAdaptor(...);
ITCSignal0 sig;
sig.connect(boost::bind(&Base::print, adpt));

즉, boost::bind()를 사용해서 SlotType의 객체를 하나 만들어 줘야 합니다. 이렇게 할 경우, boost::bind() 사용법에 익숙해 져야해야 합니다. boost::bind에 익숙하지 않은 분은 위와 같은 코드를 작성하기 힘듭니다. 게다가, adpt 인자 위치와 &Base::print 인자 위치가 서로 바뀌었으면 좋겠더군요. 다음과 같이 할 수 있다면 훨씬 자연스럽게 느껴질 것 같더군요.

sig.connect(adpt, &Base::print);

adpt->print() 하는 거랑 비슷하게 느껴지지 않으세요?

ITCSignal3의 경우에는 코드가 더 복잡해 집니다. 다음과 같은 클래스가 선언되어 있다고 가정해 보겠습니다.

struct ButtonHandler
{
    void onClick3D(int x, int y, int z)
    {
        cout << "button clicked at (" << x << ", " << y << ", " << z << ")" << endl;
    }
};

typedef ITCAdaptor<ButtonHandler> ButtonHandlerITCAdaptor;

boost::bind를 써서 ITCSignal3와 onClick3D를 연결하려면 다음과 같이 작성해야 합니다.

ITCSignal3<int, int, int>btnsig3;
btnsig3.connect(boost::bind(ITCSignal3<int,int,int>::DoIt<ButtonHandlerITCAdaptor,
                                ButtonHandler>::Type(&ButtonHandlerITCAdaptor::doIt<int,int,int>),
                                &btnHdlr, &ButtonHandler::onClick3D, _1, _2, _3));

상당히 복잡하죠? 특히나 빨갛게 표시한 부분 때문에 눈이 휘둥그래지실 겁니다. 저렇게 복잡해진 이유는 ButtonHandlerITCAdaptor가 ITCAdaptor<ButtonHandler>로부터 상속을 받아 ButtonHandler::onClick3D를 wrapping하고 있는 멤버 함수를 따로 정의하고 있지 않기 때문입니다. 그래서 ButtonHandlerITCAdaptor::doIt<int,int,int> 멤버 함수를 btnsig3에 연결하되, &btnHandler 인스턴스 및 &ButtonHandler::onClick3D 멤버 함수를 추가적인 인자로 넘겨줘야 합니다. 그리고, 나중에 btnsig3를 발생시킬 때, int, int, int 세개를 인자를 받아들이기 위해 _1, _2, _3 를 써서 인자를 위한 자리를 마련해 둡니다. 이 생각을 그냥 코딩으로 옮긴다면, 다음만으로 충분할텐데,

btnsig3.connect(boost::bind(&ButtonHandlerITCAdaptor::doIt<int,int,int>, &btnHdler,
                                 &ButtonHandler::onClick3D, _1, _2, _3));

왜 복잡하게 보이는 ITCSignal3<int,int,int>::DoIt<ButtonHandlerITCAdaptor, ButtonHandler>::Type 이런식으로 복잡한 형변환을 하고 있는 걸까요? 그건 doIt이 overload된 멤버 함수이기 때문입니다. 좀 더 상세히 설명하자면 overload된 멤버 함수는 단순히 이름만으로는 그 포인터를 얻어낼 수가 없기 때문에 그 타입 정보를 정확히 알려줘야 합니다. 예를 들어, 일반 함수가 다음과 같이 overload되어 있다면,

void foo(int);
void foo(double);

overload 된 함수 포인터를 얻어내기 위해서는 다음과 같이 코드를 작성해야 합니다.

typedef void (*FuncType1)(int);
typedef void (*FuncType2)(double);

FuncType1 fn1 = &foo;
FuncType2 fn2 = &foo;
boost::function<void (int)> f1 = FuncType1(&foo);
boost::function<void (double)> f2 = FuncType2(&foo);

즉, l-value를 통해서 타입을 알려주던지 아니면 명시적 형변환을 사용해서 타입을 알려줘야 합니다. 그런데, &ButtonHandlerITCAdaptor::doIt<int,int,int> 이 멤버 함수의 타입을 그대로 풀어쓰면 이렇게 됩니다.

void (ButtonHandlerITCAdaptor::*)(void (ButtonHandler::*)(int, int, int), int, int, int)

말로 풀어쓰자면 int, int, int 인자를 갖고 리턴형식은 void인 ButtonHandler의 멤버 함수 포인터(void (ButtonHandler::*)(int, int, int))와 더불어 int, int, int 인자를 갖고 리턴형식은 void인 ButtonHandlerITCAdaptor의 멤버 함수 포인터입니다. 이해하기가 무척 어렵습니다. 이해를 돕기 위해 이걸 typedef를 이용해서 두 단계로 정의하면,

typedef void (ButtonHandler::*CalleeMemFunPtr)(int, int, int);
typedef void (ButtonHandlerITCAdaptor::*CallerMemFunPtr)(CalleeMemFunPtr, int, int, int);

가 되겠지요. 이걸 ITCSignal3<ArgType1, ArgType2, ArgType3>에 맞게 좀더 일반적으로 정의하기 위해 ITCSignal3 템플릿 클래스에 다음과 같은 typedef 를 정의했습니다.

template <typename ArgType1, typename ArgType2, typename ArgType3>
class ITCSignal3
{
public:
    ......
    template <typename Caller, typename Callee = Caller>
    struct DoIt
    {
        typedef void (Callee::*CallType)(ArgType1, ArgType2, ArgType3);
        typedef void (Caller::*Type)(CallType, ArgType1, ArgType2, ArgType3);
    };
    ......
};

즉, ITCSignal3<ArgType1,ArgType2,ArgType3>::DoIt<Caller,Callee>::Type 을 정의한 것이지요. 이 Type이 다음과 같은 타입을 일반적으로 표현한 타입이 됩니다.

void (ButtonHandlerITCAdaptor::*)(void (ButtonHandler::*)(int, int, int), int, int, int)

그래서 다음과 같이 복잡한 표현이 나오게 된 것입니다.

ITCSignal3<int,int,int>::DoIt<ButtonHandlerITCAdaptor,
    ButtonHandler>::Type(&ButtonHandlerITCAdaptor::doIt<int,int,int>)

위와 같이 일반적인 타입을 정의하고 나니, connect() 멤버 함수를 좀 더 편하면서도 타입 인자에 따라서 자동적으로 타입이 계산되게 만들 수 있었습니다.

template <typename ArgType1, typename ArgType2, typename ArgType3>
class ITCSignal3
{
public:
    ......
    template <typename Caller, typename Callee = Caller>
    struct DoIt
    {
        typedef void (Callee::*CallType)(ArgType1, ArgType2, ArgType3);
        typedef void (Caller::*Type)(CallType, ArgType1, ArgType2, ArgType3);
    };
    template <typename Caller, typename Callee>
    ConnectionType connect(Caller* inst,
                           typename DoIt<Caller, Callee>::CallType call)
    {
        return connect(boost::bind(typename DoIt<Caller,Callee>::Type(
                                        &Caller::template doIt<ArgType1, ArgType2, ArgType3>),
                                   inst, call, _1, _2, _3));
    }
    ......
};

위와 같이 connect() 멤버 함수를 정의하게 되면, 다음과 같이 좀 더 쉽게 boost::bind()를 쓰지 않고도 connect()를 호출할 수 있게 됩니다.

btnsig3.connect<ButtonHandlerITCAdaptor, ButtonHandler>(&btnHdlr,
        &ButtonHandler::onClick3D);

원래의 connect()  멤버 함수를 사용할 때보다는 훨씬 간단해 졌습니다. 단, connect()에 타입 인자를 명시해야만 제대로 컴파일입니다.

이 정도면 ITCSignal의 핵심적인 아이디어는 모두 설명드린 것 같습니다. 다른 클래스 및 멤버 함수들은 이 글에서 설명드린 내용의 반복이기 때문에 자세한 설명은 생략하도록 하겠습니다.

다음 개선사항으로는 리턴값이 있는 함수를 지원하는 기능을 추가해 볼 생각입니다.

혹시 잘못된 부분이나 개선사항이 있으시면 얘기 좀 해주세요~

여느때와 같이 코드 첨부합니다. 이해되지 않는 부분에 대한 질문도 환영합니다. ^^