[C++] An Eventsystem Without std::bind / std::function

Find this example on my GitHub repository.

I've been thinking about Eventsystems since a while. I've read a couple of different articles and didn't find a satisfying solution. Finally, with the Hazel Engine I found one, an eventsystem without using std::function and std::bind.

In this example we consider the customers and stock of a book store. Those are represented in two classes, where we provide a public OnEvent.

// Customers.hpp
class Customers {
public:
    void OnEvent(Event& event);
private:
    void onNewCustomer(NewCustomerEvent& newCustomerEvent);
    void onCustomerDeleted(DeleteCustomerEvent& deleteCustomerEvent);
};

// Stock.hpp
class Stock {
public:
    void OnEvent(Event& event);
private:
    void onBookAdded(BookAddedEvent& bookAddedEvent);
    void onBookRemoved(BookRemovedEvent& bookRemovedEvent);
};

OnEvent(Event& event) takes an abstract event. After the call we want to get the concrete event and forward it to the specific functions onBookAdded(..) and onBookRemoved(..).

In this article I'll explain the eventsystem with the NewCustomerEvent. The entire example has four different events:

- new customer
- customer deleted
- book added to stock
- book removed from stock
 

Event and EventType

We'll have an enum class of our defined event types and an interface Event which all concrete events then inherit.

enum class EventType {
    none = 0,
    BookAddedEvent, BookRemovedEvent,
    NewCustomerEvent, DeleteCustomerEvent
};

class Event {
public:
    virtual ~Event() = default;

    virtual EventType GetType() const = 0;
    virtual const char* GetName() const = 0;
    bool Handled = false;
};

Let's implement the first concrete event. Therfore we need to do:

  1. Implement pure virtual functions from Event
  2. Add concrete members (data we'll use)
  3. Add a static memberfunction GetConcreteType to get the concrete event type

The static function GetConcreteType is essential for dispatching the event later. We can't provide static functions in an interface and to make it less error prone we'll create a macro for implementing the GetName, GetType and a static GetConcreteType.

#define EVENT_CLASS_TYPE(type) \
static EventType GetConcreteType() { return EventType::type; }\
EventType GetType() const override { return GetConcreteType(); }\
const char* GetName() const override { return #type; }

The static EventType GetConcreteType() does it's job like the name says. It returns the type (from the enum EventType) of the event.

So using the macro simplifies the implementation for NewCustomerEvent and we just provide members and a constructor:

class NewCustomerEvent : public Event {
public:
    NewCustomerEvent(std::size_t Id, const std::string& Name, const std::string& Address) :
        id(Id), name(Name), address(Address) {}
    std::size_t id;
    std::string name;
    std::string address;
    EVENT_CLASS_TYPE(NewCustomerEvent);
};
 

Dispatcher

The Dispatcher will dispatch the events and trigger a desired function. Consider following implementation:

class Dispatcher  {
public: 
    Dispatcher(Event& e ) : event(e) {}
    
    template<typename T, typename F> 
    void Dispatch(const F& function) {
        if (event.GetType() == T::GetConcreteType()) {
            function(static_cast<T&>(event));
            event.Handled = true;
        }
    }
private:
    Event& event;
};

We hold the Event as member in the Dispatcher and use Dispatch to - guess what - dispatch them. Let's break this function down line by line:

  • We specify T on the function call which will be the concrete Event Type (e.g. dispatcher.dispatch<NewCustomerEvent>(..))
  • F will be deduced by the compiler from the argument and we don't need to specify it
  • F is our function which we call in the followed if statement
  • We check if a given event has the same type as the specified event from T (this is the static member function of concrete events)
  • We call function, cast the event to it's concrete type and pass it
  • We set Handled to true so we know later that the event was handled
 

Create And Dispatch An Event

Imagine you provided this code as library and a client is calling a function where you then create NewCustomerEvent. Remember the class Customers from the beginning? You'll pass the event to the public memberfunction OnEvent. Consider the following code snippet:

// an arbitrary function called by clients
void any_api_function (/*...*/) {
    // pretend customers is available to call OnEvent(..);
    NewCustomerEvent newCustomer(123, "Thomas", "Munich");
    customers.OnEvent(newCustomer);
}

// BookStore/Customers.cpp
// public memberfunction
void Customers::OnEvent(Event& event) {
    Dispatcher dispatcher(event);
    dispatcher.Dispatch<NewCustomerEvent>(BIND_MEMBERFUNC_WITH_EVENT(Customers::onNewCustomer));
    dispatcher.Dispatch<DeleteCustomerEvent>(BIND_MEMBERFUNC_WITH_EVENT(Customers::onCustomerDeleted));
}
// private memberfunction
void Customers::onNewCustomer(NewCustomerEvent& newCustomerEvent) {
    std::cout << "New customer event: " << newCustomerEvent.GetName() << 
    "\n    Id: " << newCustomerEvent.id << 
    "\n    Name: " << newCustomerEvent.name << 
    "\n    Address: " << newCustomerEvent.address << 
    '\n';
}

I highlighted the call order from top to bottom. We create the event and call the appropriate OnEvent method in customers. We pass the event to it and create Dispatcher in the local space. Dispatcher takes the event and if the given template type matches the type in event we'll trigger the binded function. We bind the memberfunction onNewCustomerEvent to it and for demonstration purpuses I added some simple outputs here.

 

Function Binding

Let's consider the function binding BIND_MEMBERFUNC_WITH_EVENT(..):

#define BIND_MEMBERFUNC_WITH_EVENT(member_function) [this](auto&&... args) \
        { return this->member_function(std::forward<decltype(args)>(args)...); }
// which expands in our example to:   
[this](auto&&... args){ 
    return this->Customers::onNewCustomer(std::forward<decltype(args)>(args)...); 
}

The function binding is done as described in Effective Modern C++ Scott Meyers, Item 33: Use decltype on auto&& parameters to std::forward them. So we created a generic lambda and simply forward an arbitrary event here. We don't need std::function or std::bind. And this is passed to the Dispatcher where this lambda and therefore the binded memberfunction finally is called.

And since we use this binding for all concrete events, we need to define the lambda arguments as auto&& and use decltype and std::forward accordingly.

 

Conclusion

Find this example on my GitHub repository.

I genuinly like the idea of this eventsystem. You can define specific events and pass them to the desired location to trigger a certain function. Also avoiding std::function and std::bind is a real benifit here. Check out the entire example on my GitHub repo and get the idea behind. There are four different events and two consumers (Customers and Stock).

I added another function to Dispatcher if you want to call a function without arguments (DispatchWithoutEvent). If needed, you can also change the returntype from Dispatch to bool (to check if the event was dispatched right after the call).

There are also other bindings to call regular functions with and without an event. Play around and have fun.

That's it for now.

Best Thomas

Previous
Previous

[C/C++] A Lua C API Cheat Sheet

Next
Next

[C++] Typed Tests For Interfaces With Googletest