c++ message dispatcher

16 Nov 2013

c++ template study

간단한 message dispatch 코드를 c++로 구현해보자. 일단 std::function을 안 쓰고 구현해보고, 그 다음에 std::function을 써서 구현해보자.

지난 번 글에서 functor_iihandler_t의 근간을 이미 설명했다.

template <typename R, typename T>
struct handler_t {
    handler_t()
        : _impl(nullptr) {
    }
    template <typename F>
    handler_t(F f)
        : _impl(new impl_t<F>(f)) {}

    R operator () (T arg) const {
        return (*_impl)(arg);
    }
private:
    struct wrapper_t {
        virtual R operator () (T arg) const = 0;
    };
    template <typename F>
    struct impl_t : public wrapper_t {
        impl_t(F f)
            : _f(f) {}
        virtual R operator () (T arg) const {
            return _f(arg);
        }
        F _f;
    };
    std::shared_ptr<wrapper_t> _impl;
};

interface인 wrapper_t, 실 구현체인 impl_t, 그리고 그것을 멤버로 갖는 type erasure가 적용된 대표 type인 handler_t이다. 1개의 인자와 반환 값을 가질 수 있으므로 이를 template parameter인 RT로 표현한 것이다.

각 message handler가 받을 message의 최상위 class를 만들고, 추후 코딩을 편하게 하기 위해 handler에 대한 typedef를 걸어준다.

struct message_t {
    virtual ~message_t() {}
};
typedef handler_t<void, const message_t&> msg_handler_t;

이제 모든 작업이 완료되었다. msg_handler_t에 대한 unordered_map 객체 만들고, 적절히 불러주기만 하면 된다.

std::unordered_map<int, msg_handler_t> __handler_table;
void dispatch(int opcode, const message_t& msg) {
    __handler_table[opcode](msg);
}

std::function을 사용할 경우 template parameter만 function type으로 넣어서 msg_handler_t에 대한 typedef을 바꿔주면 된다.

typedef std::function<void (const message_t&)> msg_handler_t;

표준 라이브러리에서 인자를 저렇게 받는 이유는 저 표현식이 단순히 반환 타입, 인자 타입을 나열하는 것에 비해 더 가독성이 좋다고 판단했기 때문이다. 저렇게 받은 인자는 내부에서 function_traits에 의해 다시 반환 타입과 인자 타입이 분리되어 결국 handler_t와 같은 구현을 하게 된다.

위와 같이 코드를 구현하면 조금 아쉬운 점이 있다. 예를 들어 message_t를 상속받은 int_msg_tstring_msg_t가 있다고 하자. 그리고 이를 처리하는 int_msg_handler, string_msg_handler가 있을 때 코드는 다음과 같다.

void int_msg_handler(const message_t& m) {
    const int_msg_t& msg = static_cast<const int_msg_t>(m);
    // do something with int_msg
}
void string_msg_handler(const message_t& m) {
    const string_msg_t& msg = static_cast<const string_msg_t>(m);
    // do something with string_msg
}
enum msg_id {
    int_msg_id,
    string_msg_id,
};
void main() {
    __handler_table[int_msg_id] = int_msg_handler;
    __handler_table[string_msg_id] = string_msg_handler;
}
  • 먼저, 각 handler의 type은 모두 void (const message_t&) 이기 때문에 실제 우리가 원하는 type으로 인자를 받을 수가 없다. 따라서 매번 casting해주는 과정이 필요하다.
  • 그리고 각 msg가 추가될 때마다 msg_id 값을 정의해주어야 한다.
  • 마지막으로 구현한 handler를 handler_table에다가 등록해주는 코드를 작성해야 한다.

일단 msg_id 문제를 해결해보자. 여러가지 방법이 있겠지만 귀찮으니 type_index를 사용하자.

std::unordered_map<std::type_index, msg_handler_t> __handler_table;

message_t를 받는 함수와 원하는 type을 받는 함수를 분리하고, __handler_table에 함수를 프로그램 시작 시에 등록하기 위해 static 전역 변수를 사용해 코드를 정리하면 다음과 같다.

template <typename _Ty>
const _Ty& cast(const message_t& msg) {
    return static_cast<const _Ty&>(msg);
}
static void int_msg_handler(const int_msg_t& msg);
static void _int_msg_wrapper(const message_t& m) {
    int_msg_handler(cast<int_msg_t>(m));
}
static void int_msg_handler(const int_msg_t& msg) {
    // do something with int_msg_t
}
static struct _table_register_t {
    _table_table_t() {
        __handler_table.insert(std::make_pair(
            std::type_index(typeid(int_msg_t)), _int_msg_wrapper));
    }
} __register1;

일단 편하게 message를 casting하기 위한 cast 함수를 만들었다. 그냥 const reference 지키면서 static_cast를 해주는 함수이다.

  • dynamic_cast를 하지 않은 이유는 항상 올바른 type만 casting을 요청할 것이라는 믿음을 갖고 불필요한 검사를 피하기 위함이다
  • _Ty가 정말 message_t를 상속받았는지 보려면 std::is_base_of를 쓰면 되겠다.

실제 작업을 수행할 함수인 int_msg_handler를 선언한다. 그리고 전달 함수인 _int_msg_wrapper 함수를 작성한다. _int_msg_wrapper 함수에서는 message_tint_msg_t로 casting만 해서 int_msg_handler 함수로 넘겨준다. 그리고 실질적인 작업은 int_msg_handler에서 int_msg_t를 인자로 받아 처리하게 된다.

프로그램 시작과 동시에 __handler_table에 등록하는 가장 좋은 방법은 전역 객체의 생성자를 사용하는 방법이다. 전역으로 정의된 객체의 생성자는 프로그램 시작 시에 호출된다는 점을 이용하는 것이다. 따라서 전역 객체 __register1를 정의한다. 그러면 프로그램 시작 시 _table_register_t의 생성자가 호출되고, 그 생성자에서 __handler_table에게 int_msg_t에 대한 type_index_int_msg_wrapper를 넣어주면 된다.

그런데 위 코드를 보면 int_msg_t에 해당하는 부분을 string_msg_t로 치환했을 때, string_msg_t를 위해서도 사용할 수 있는 것을 확인할 수 있다. 즉, 대부분의 코드가 무의미하게 반복된다는 것이다. 이러한 boilerplate code를 적절히 제거하기 위해 반복되는 부분을 다음과 같이 macro로 묶어내자.

template <typename _Ty>
struct table_register_t {
    table_register_t(msg_handler_t handler) {
        handler_table().insert(std::make_pair(std::type_index(typeid(_Ty)), handler));
    }
};
#define HANDLER(msg_type) \
    static void msg_type##_handler(const msg_type& msg); \
    static void _##msg_type##_wrapper(const message_t& m) { \
        msg_type##_handler(cast<msg_type>(m)); \
    } \
    static table_register_t<msg_type> __reg_##msg_type##(_##msg_type##_wrapper); \
    static void msg_type##_handler(const msg_type& msg)

이제 다음과 같이 사용할 수 있다.

struct int_msg_t : public message_t {
    int a, b;
};
HANDLER(int_msg_t) {
    std::cout << msg.a + msg.b << std::endl;
}

이제 message를 하나 만들어서 날려보자. 이를 처리하는 dispatch 함수는 다음과 같이 간단하게 작성할 수 있다.

template <typename _Ty>
void dispatch(const _Ty& msg) {
    __handler_table[std::type_index(typeid(_Ty))](msg);
}

void main() {
    int_msg_t msg;
    msg.a = 100;
    msg.b = 20;
    dispatch(msg);
}

아무 message 객체나 받아서 그 type_index로 적절한 handler를 찾고, 그 handler에게 message 객체를 넘겨서 처리될 수 있도록 하는 것이다.

하지만 위 코드에는 문제가 있다. 전역 table_register_t 객체와 __handler_table 객체가 다른 번역 단위에 있을 경우, 두 객체의 생성자가 불리는 시점이 undefined이기 때문에 아직 __handler_table 객체가 초기화되지 않은 시점에서 register를 수행하다가 프로그램이 죽는 문제이다.

이 문제를 해결하는 가장 쉬운 방법은 handler_table 객체를 전역 변수 말고 singleton으로 만드는 것이다. 원칙적으로 singleton 객체는 처음 접근할 때 생성되기 때문에, 어떤 register 객체든 handler_table 객체를 처음 접근하는 때 handler_table 객체를 생성하게 되니 위 문제를 해결할 수 있다.

그리고 c++11에서는 간단하게 singleton을 구현할 수 있다.

inline handler_table_t& handler_table() {
    static handler_table_t _handler_table;
    return _handler_table;
}

코드 전문은 다음과 같다.

struct message_t {
    virtual ~message_t() {}
};

typedef std::function<void (const message_t&)> msg_handler_t;
typedef std::unordered_map<std::type_index, msg_handler_t> handler_table_t;

inline handler_table_t& handler_table() {
    static handler_table_t _handler_table;
    return _handler_table;
}

template <typename _Ty>
inline const _Ty& cast(const message_t& msg) {
    return static_cast<const _Ty&>(msg);
}

template <typename _Ty>
struct table_register_t {
    table_register_t(msg_handler_t handler) {
        handler_table().insert(std::make_pair(std::type_index(typeid(_Ty)), handler));
    }
};

#define HANDLER(msg_type) \
    static void msg_type##_handler(const msg_type& msg); \
    static void _##msg_type##_wrapper(const message_t& m) { \
        msg_type##_handler(cast<msg_type>(m)); \
    } \
    static table_register_t<msg_type> __register_##msg_type##(_##msg_type##_wrapper); \
    static void msg_type##_handler(const msg_type& msg)

template <typename _Ty>
inline void dispatch(const _Ty& msg) {
    handler_table()[std::type_index(typeid(_Ty))](msg);
}

그리고 main 함수에서는 다음과 같이 사용한다.

struct int_msg_t : public message_t {
    int a, b;
};
HANDLER(int_msg_t) {
    std::cout << msg.a + msg.b << std::endl;
}

void main() {
    int_msg_t msg;
    msg.a = 100;
    msg.b = 20;
    dispatch(msg);
}

정리

이번 과제에서는 간단하게 c++의 message dispatch 코드를 구현해보았다. 위와 같은 코드는 queue를 붙여서 message pattern이나 event pattern을 구현할 때 쓰이거나 네트워크를 붙여서 서버의 packet dispatch 코드로도 사용된다. 언듯 보면 복잡해보일 수도 있으나 실상은 function map이고, 여기저기서 자주 만나게 될 개념이니 이번 기회에(?) 익숙해지도록 하자.

추가 과제였던 reflection 과제에 대한 풀이는 예전에 작성한 글에 대한 링크로 대신하겠다.

comments powered by Disqus