Applied Event-driven programming with C++

Shubham Anand
4 min readFeb 21, 2023

--

Photo by Joshua Reddekopp on Unsplash

Event-driven programming is a popular programming model where instead of the usual execution of instructions in sequential order, the flow control of the code is determined via events. Event-driven programs should be able to detect and react to events in near real-time.

One of the most popular examples of an event-driven model is our web browser. We make extensive use of event listeners on the web to respond to browser or user events for delivering an immersive experience for the end users. The event-driven model can easily break down complex application logic into simple instructions.

Most of the event-driven models work on the concept of callbacks. If you’re from the domain of web development, you might already be familiar with what a callback is. Let’s take a dive into how an event-driven programming model functions at its core. We’ll borrow some basic STLs from C++ to help implement us a simple but very powerful EventManager Interface.

EventManager Interface

We’ll implement a very basic EventManager interface in C++. Before we can start, let’s understand and iron out the basic things we’ll be needing.

Just like javascript, functions in C++ are first-class citizens. This fancy term means that functions can be stored in variables and can also be passed around to other functions! In C/C++ we can use a pointer to a function to store a reference to the function. Let’s say we want to store a reference to a function in fptr , which returns an integer and takes two float arguments, our syntax for the same would be int (*fptr)(float, float) . You can learn more about function pointers here.

We’ll be using std::map STL for storing all registered events and their listeners. Our events object would store “event_name” as the key and std::vector of function pointers as value. Below is a sample structure of our events object, where f1, f2, f3, f4, and f5 are pointers to functions.

{
"event1": [fp1, fp2, fp3],
"event2": [fp4, fp5]
}

For this tutorial, we’ll be assuming our callbacks will have the signature of void (*callback)(int) . You can even have variable arguments to a callback :)

We’ll have two member functions in our EventManager interface:

  • EventManager *on(std::string event_name, void (*callback)(int))
    This method will take in an event_name along with a callback and register this callback as one of the listeners to the event. It’ll return a pointer to itself. This will help us chain the method calls. Eg -> event_manager->on(..)->on(..)->emit(..)
  • bool emit(std::string event_name, int arg)
    This method will take an event_name and an argument. This will invoke all the listeners that are associated with this event and will return true if emitting succeeded. It’ll pass the arg to our callback functions.

Now, we’re ready to look at the implementation,


// app/event_manager.hpp

#ifndef EVENT_MANAGER
#define EVENT_MANAGER

#include <algorithm>
#include <vector>
#include <map>

class EventManager {

private:
std::map<std::string, std::vector<void (*)(int)> > events;

public:
EventManager() {}

EventManager *on(std::string event_name, void (*callback)(int)) {

// we're using a pointer to reference `events[event_name]` so as
// to get reference to original object and not the copy object.
std::vector<void (*)(int)> *listeners = &events[event_name];

// if this listener is already registered, we wont add it again
if (std::find(listeners->begin(), listeners->end(), callback) != listeners->end()) {
return this;
}

listeners->push_back(callback);

return this;
}

bool emit(std::string event_name, int arg) {
std::vector<void (*)(int)> listeners = events[event_name];

if (listeners.size() == 0) return false;

// Run all the listeners associated with the event
for (int idx = 0; idx < listeners.size(); idx += 1) {
listeners[idx](arg);
}

return true;
}
};

#endif

Testing our Interface

Let’s use the following simple snippet to test out our interface

We’re able to attach multiple listeners to a single event which gives us immense flexibility in what we want to do when a particular event is fired. In fact, by using a priority queue instead of an array, we can even define an order in which event execution should occur post-firing of the event.

EventManager interface could be used to create global event managers as well. So you can fire events from multiple files and define callbacks that should be executed on certain events. A few of the use cases of this powerful pattern are as follows

  • Modeling Lifecycle Methods: Designing lifecycle methods in your program like “before_end”, “after_end”, and “after_render” can be very simple by leveraging event-driven programming.
  • Reacting to User Events: Events like mouse clicks, keystrokes, and other user actions are best modeled by event-driven programming

References/Further Reading

--

--