3
\$\begingroup\$

I've implemented an events system in C++11. I've now got it pretty much as good as I can get it. It feels like I have found an optimal design pattern, and I'm using sensible variable names to illustrate what is going on.

Still not sure what the best words are: e.g. Invoker, Dispatcher, or Notifier? Event.fire() or .publish() or .trigger()? Reciever, Listener, or Subscriber? etc...

As is a fairly involved machinery, I've taken trouble to document. I would be very grateful if anyone can offer more concise or improved documentation, as I find that my ability to document something starts to decrease once I get close to fully understanding it, as I forget the sequence of key observations necessary to grok the structure. What would you modify if you were to assimilate this into your code base?

Coliru

// C++11 Event System
// π 06.03.2016
// 5th revision

#include <vector>
#include <iostream>
#include <algorithm>
#include <memory>
#include <string>

using namespace std;
/*
 Usage:

    Event<int> event;

    class Reciever : EventManager<Reciever> {
        void handler(int k) {...}

    reciever.connect(event, &Reciever::handler);

    event.fire(42);

Internals:

Terminology
    An Invoker is a machine for invoking a particular member function of a particular instance of a particular object type.
    e.g. 
        class T { void f(int i) {cout<<i;} };  T t;   I = Invoker(&t, &T::t);   I(42);

    We have two principal objects:
        Event<Args…> needs to hold a list of Invokers (as InvokerBase*), such that when the event fires, 
          every connected invoker does its invoke().
        EventManager<T> needs to hold a list of Event* (as EventBase*) so that when a listener is destroyed, 
          all events connecting into it are told to remove all relevant invokers.

        We have to use a InvokerBase base class as invokers of potentially of different types
        We have to use an EventBase base class as events are potentially of different types

Details:    
    Event<Args…> contains a list of Invoker<T>-s, each invoker holding a T* t and a void(T::f*)(Args…). 
    Event<Args…> also contains a void fire(Args… args), which calls t->f(…args) on every invoker.
    Note that two invokers may have different T-s. So how to store a list of such?
    Answer: make them all inherit from a common InvokerBase<Args…> which contains a virtual call(Args… args)

    So why do we need an EventManager?  Can't we just have an Event class that does the above?
    What happens if a receiver is destroyed?  Next time the event fires it will attempt to invoke 
      a method on an instance that no longer exists. UB hazard alert!
    We could provide Event with a removeInvoker(void* t) method, and require the consumer to invoke 
      this from its destructor.  But it would be nicer & safer to have this happen automatically.
    So our listener class T will derive from EventManager<T>, and in EventManager's destructor 
      we can call removeInvoker(this) for every event that T has been connected to.
    In order to do this, EventManager is obviously going to need to maintain a list of such events.
    And again, we have the same problem of holding on a list of different events, maybe Event<float>, Event<int,bool>, etc.
    So again we will have to make an EventManagerBase, containing a virtual removeInvoker(void*) which EventManager will override.
    Luckily we can cast to a void* — given a particular foo instance, we can request an event to remove every invoker that points to this object.
*/


// [1c] Each invoker is for a different object type, so how to store them in a list?
//    Answer: Make a base class!
template<class... Args>
class InvokerBase {
public:
    virtual void call(Args... args) = 0;
    virtual void* getInstance() = 0;
    virtual ~InvokerBase() { /* derived dtor executes before hitting here */ } // deleting base object invokes derived dtor
};

// [1b] A invoker has to invoke a method on an instance of some object, and hence needs:
//       - a pointer to the object,
//       - a pointer to the member function that needs to be invoked
//    When it fires, each is invoked.
template<class T, class... Args>
class Invoker : public InvokerBase<Args...> {
private:
    T* t;
    void(T::*f)(Args...);
public:
    Invoker(T* t, void(T::*f)(Args...)) : t(t), f(f) { }
    void call(Args... args)   final { (t->*f)(args...); }
    void* getInstance()       final { return (void*)t; }
    ~Invoker()                final { cout << "~Invoker() hit! \n"; }
};


class EventManagerBase {
public:
    virtual void removeEvent(void* ev) = 0;
};

// [2c] As these events will have different sigs, again we need the same base-class trick.
class EventBase { 
public:
    virtual void removeInvoker(void* t) = 0;
};

template<class... Args>
class Event : public EventBase {
private:
    // Note: When the instance is destroyed, or items removed from vector, delete is called relevant element(s) thanks to unique_ptr
    using InvokerBase_smartPointer = unique_ptr<InvokerBase<Args...>>;
    // [1a] An event holds a list of invokers.
    std::vector<InvokerBase_smartPointer> invokers;
public:
    // [5] boum! 
    void fire(Args... args) {
        for (auto& i : invokers)
            i->call(args...);
    }

    template<class T>
    void addInvoker(T* t, void(T::*f)(Args... args)) {
        auto i = new Invoker <T, Args...>(t, f);
        invokers.push_back(InvokerBase_smartPointer(i));
    }

    void removeInvoker(void* t) final {
        auto to_remove = std::remove_if(
            invokers.begin(),
            invokers.end(),
            [t](auto& i) { return i->getInstance() == t; }
            );
        invokers.erase(to_remove, invokers.end()); // yup std::remove_if doesn't actually do the removing (gah)
    }

    // [3] ok, So we have the EventManager cleaning up after itself. Now let's get Event to also do that.
    ~Event() {
        for (auto& i : invokers) 
            ((EventManagerBase*)(i->getInstance())) -> removeEvent(this);
    }
};

// [2a] The consumer's Reciever must derive from EventManager<Reciever>
template<class Derived>
class EventManager : public EventManagerBase {
private:
    // [2b] ... which holds a list of all events holding a subscription to us...
    std::vector<EventBase*> events;

public:
    // [4] This is the heart of the operation.
    //  Simultaneously add listener to event's invokers while adding event to the listeners event-list.
    template<class... Args>
    void connect(Event<Args...>& ev, void(Derived::*listenerMethod)(Args... args)) {
        ev.addInvoker((Derived*)this, listenerMethod); // [1a]
        events.push_back(&ev);
    }

    void removeEvent(void* ev) final {
        auto to_remove = std::remove_if(
            events.begin(),
            events.end(),
            [ev](auto& elt) { return elt==(EventBase*)ev; }
            );
        events.erase(to_remove, events.end()); // yup std::remove_if doesn't actually do the removing (gah)
    }

    // [2d] ...so that when the reciever dies, we notify each event to remove it's subscription.
    ~EventManager() {
        for (auto& e : events)
            e->removeInvoker((void*)this);
    }
};

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 

// [0] Example usage:
class Sender {
private:
    string name;
public:
    Event<float> eventFloat;
    Event<bool, int> eventB; // etc.

    Sender(string s) : name(s) { }

    void triggerEvent() {
        cout << name << "::triggerEvent() ~ Firing event with: 42\n";
        eventFloat.fire(42.0f);
    }
};

struct Reciever : EventManager<Reciever> {
    string name;
    Reciever(string s) : name(s)  { cout << s << "()\n"; }
    ~Reciever()  { cout << "~" << name << "()\n"; }

    void gotEvent(float x) { cout << name << "::gotEvent hit with value: " << x << endl; }
};

int main() {
    // Event source and listener unaware of each other's existence.
    Sender sender("sender");
    Reciever reciever1("reciever1");

    reciever1.connect(sender.eventFloat, &Reciever::gotEvent);

    {
        Reciever reciever2("reciever2");
        reciever2.connect(sender.eventFloat, &Reciever::gotEvent); 
        sender.triggerEvent(); // get 2 hits
    }

    sender.triggerEvent(); // get 1 hit

    return 0;
}
\$\endgroup\$
1
  • \$\begingroup\$ So you are connecting a receiver/listener to an invoker/dispatcher? Most implementations I have seen (Qt signal/slot, Java Event handler, ...) do it the other way around. \$\endgroup\$ Commented Mar 10, 2016 at 9:34

1 Answer 1

3
\$\begingroup\$

Design OVerview

I think your naming convention is slightly different from normal:

// I would have called this the EventSource.
// The object that events are coming from.
// Because this is the object the fires an event.
Event<int> event;

// I would have called this the EventListener
// The object that listen for and react to event actions.
// But note: They are not necessarily the exclusive receiver of an event
//           Which "Receiver" implies.
class Reciever : EventManager<Reciever> {
    void handler(int k) {...}

// Here "42" is the Event that is happening.
event.fire(42);

I also think you interface for registering is convoluted:

reciever.connect(event, &Reciever::handler);

I suppose you can do it this way. But I would have registered the receiver with the class that is generating the events (because there could be more than one receiver of events).

event.connect(reciever); // Should not even need to tell them what
                         // function to call that is implied by the
                         // type of event.

Code Review

Never do this

using namespace std;

Especially since this is a header file that will be included by other people's code.

If I include this into my code this change can break how my code works. Most projects will ban the use of your project if you put this in the header file for this reason. So don't do it.

Even putting this line in a source file is bad idea. It can silently cause breaks in your code. This is a good article about the issue.Why is “using namespace std;” considered bad practice?

Std::function

Your class InvokerBase and Invoker seem to be reinventing the std::function class. The use of lambdas and std::function have basically eliminated the need for these types of class. Please use the classes that have been provided by the standard.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.