Skip to content

Implementing EventEmitter

d5 edited this page Feb 9, 2012 · 7 revisions

This is what we need in the end:

class HTTP : public EventEmitter {
    // ..
}

HTTP h;

// subscription
h.addListener("request", [=](/*...*/) { /*...*/ });
// Or,
h.addListener<request>([=](/*...*/) { /*...*/ });

// invoke
h.emit("request", /*...*/);
// Or,
h.emit<request>(/*...*/);

In above code, template argument in the latter patter can be anything: int, class type...

The objectives are:

  • Easy to use
  • Event emitters must be able to validate event name(or id): adding listeners for 'hello' event to HTTP instance should not be allowed.
  • Class inheritance and events must be related to each other.

Anyway, to achieve this, I tried many various ways, but nothing yet is proven to work as intended. This is my current work in progress:

class event_base
{
public:
    event_base() {}
    virtual ~event_base() {}
};

template<int id, typename callback_t>
class event : public event_base
{
    callback_t c_;

public:
    event(callback_t c) : c_(c) {}

    template<typename ...A>
    void invoke(A&& ... args)
    {
        c_(std::forward<A>(args)...);
    }
};

enum {
    event1,
    event2,
    event3,
    event4,
};

class base1
{
    typedef std::vector<std::shared_ptr<event_base>> listener_list;

public:
    base1() : map_() {}
    virtual ~base1() {}

    template<typename T, int I> struct foo {};

public:
    template<typename T, int id>
    void addEvent(typename foo<T, id>::type callback)
    {
        auto it = map_.find(id);
        if(it == map_.end()) map_.insert(std::make_pair(id, listener_list()));
        map_[id].push_back(std::shared_ptr<event_base>(new event<id, typename foo<T, id>::type>(callback)));
    }

    template<typename T, int id, typename ...A>
    void invokeEvent(A&& ... args)
    {
        typedef typename foo<T, id>::type cb_t;
        typedef event<id, cb_t> ev_t;

        auto e = map_.find(id);
        if(e != map_.end())
        {
            for(auto l : e->second)
            {
                auto f = dynamic_cast<ev_t*>(l.get());
                f->invoke(std::forward<A>(args)...);
            }
        }
    }

private:
    std::map<int, listener_list> map_;
};

class class1 : public base1 {};
template<> struct base1::foo<class1, event1> { typedef std::function<void(int)> type; };
template<> struct base1::foo<class1, event2> { typedef std::function<void(int, int)> type; };

class class2 : public base1 {};
template<> struct base1::foo<class2, event1> { typedef std::function<void(int)> type; };
template<> struct base1::foo<class2, event3> { typedef std::function<void(int, int, int)> type; };

You can see the limitation of this approach in the following usage code:

int t = 0;

class1 f;
f.addEvent<class1, event1>([&](int a){
    t++;
    std::cout << "[" << t << "] event1: " << a << std::endl;
});
f.addEvent<class1, event2>([=](int a, int b){
    std::cout << "[" << t << "] event2-1: " << a << " " << b << std::endl;
});
f.addEvent<class1, event2>([=](int a, int b){
    std::cout << "[" << t << "] event2-2: " << a << " " << b << std::endl;
});

f.invokeEvent<class1, event1>(5264);
f.invokeEvent<class1, event2>(5264, 1000);
f.invokeEvent<class1, event1>(5264);
f.invokeEvent<class1, event2>(5264, 1000);

As you can see, every function call requires evident class name as the first template parameter: class1 in the above code.

I'm still researching for better implementations that will look simpler, intuitive, and even hard-to-use-incorrectly.

Clone this wiki locally