So this is going to be my attempt at trying to explain to you guys how I came up with my event system in C++. Anyways, to start off, why do we need event systems? Well, often times in code we come across situations similar to this: when A happens, we want to do B and C. Sure, it's easy to handle with if statements if you only have a few things running at once, but what if you have many different types of objects that can have different things happening to them? Writing if code for those is going to take a while for sure.
Now that we know why we need them, let's see what they are.
An event system will contain:
- A bunch of custom events that can be triggered
- Event callback functions to handle specific events when they are triggered
- A management system to attach/remove/call event callbacks
- Objects with triggerable events
In code, this will look something like (from
Infinity):
//INFINITY_CALL is just __stdcall
namespace Infinity {
typedef INFINITY_VOID (INFINITY_CALL *EventCallback_t)(IEvent *);
///
///The event manager: implement this if a class has events that should be listened to
///
///
///The implementing project should probably write its own class implementing IEventManager to avoid writing duplicate code
///
struct IEventManager {
virtual INFINITY_VOID INFINITY_CALL AttachEventListener(INFINITY_GUID EventID, EventCallback_t f) = 0;
virtual INFINITY_VOID INFINITY_CALL DetachEventListener(INFINITY_GUID EventID, EventCallback_t f) = 0;
virtual INFINITY_VOID INFINITY_CALL TriggerEvent(IEvent *e) = 0;
};
///
///The event interface
///
struct IEvent {
virtual INFINITY_GUID INFINITY_CALL GetEventType(INFINITY_VOID) const = 0;
virtual INFINITY_VOID INFINITY_CALL GetEventData(INFINITY_VOID *pData) = 0;
};
}
First you see a generic callback function. This is the form that all event callbacks within my system will have.
Next, I essentially have a pure virtual interface that each project will implement to allow flexible control over how they want to manage their own special event system for flexibility. I also made this design choice because exporting classes with STL containers is generally not a good thing to do. I needed a way to associate certain events with certain callbacks so I took the easy route and used GUIDs to identify the different types of events that are going to potentially occur within my system. Callbacks are easily identifiable because of their addresses in memory, so that's all good.
AttachEventListener should register the specified callback with the specified event, while
DetachEventListener should do the exact opposite.
TriggerEvent does exactly what it sounds like it should - it is given an event and it should call all the callback functions that are registered to process the specified event. The only problem I foresee is that
TriggerEvent has public visibility so idiots may decide to spawn random and meaningless events.
For the event interface, we have a function that will return the GUID of the event, for identification purposes and potential casting later on in the code. We also have a function that will retrieve all the data stored with the event. How this data will be filled is left to the implementation of the event. Perhaps the data is encapsulated in a class, or maybe a struct, or maybe it's just a single byte of data. This is why we use the
void pointer.
Now that you see how I laid out the event system, you should take the time to see why this works. After that, move on down to see how I used this in my test implementation.
YAY YOU MADE IT TO MY IMPLEMENTATION
Okay, let's get serious. We need a manager base class so our objects can be created without copy pasting event management code. I decided to do something like this:
class CEventManager : public Infinity::IEventManager {
public:
CEventManager() {}
~CEventManager() {}
//GUID comparison functor
typedef struct {
bool operator()(const INFINITY_GUID &left, const INFINITY_GUID &right){
if(left.Data1 < right.Data1) return true;
if(left.Data2 < right.Data2) return true;
if(left.Data3 < right.Data3) return true;
for(register size_t i = 0; i < 8; i++)
if(left.Data4 < right.Data4) return true;
return false;
}
} GUIDComparer;
virtual INFINITY_VOID INFINITY_CALL AttachEventListener(INFINITY_GUID EventType, Infinity::EventCallback_t f){
//Just insert it
event_map[EventType].insert(f);
}
virtual INFINITY_VOID INFINITY_CALL DetachEventListener(INFINITY_GUID EventType, Infinity::EventCallback_t f){
//See if the event type has any listeners
auto it = event_map.find(EventType);
if(it != event_map.end()){
//If so, see if it has our listener function
auto func_it = it->second.find(f);
if(func_it != it->second.end()){
//Then remove it
it->second.erase(func_it);
}
}
}
virtual INFINITY_VOID INFINITY_CALL TriggerEvent(Infinity::IEvent *e){
//Launch listeners
INFINITY_GUID magic = e->GetEventType();
std::for_each(event_map[magic].begin(), event_map[magic].end(), [&] (Infinity::EventCallback_t f) -> INFINITY_VOID { f(e); });
delete e; //Because it's called with something like TriggerEvent(new IEvent(...));
}
private:
//Global event listener storage for each event
//Maps (GUID EventType) -> set of listener functions
std::map, GUIDComparer> event_map;
};
As you can see, I am mapping each GUID to a set of callbacks. I could have used a linked list, vector, or any other container, but I chose a set. However, GUIDs do not have any sort of comparator so I had to define my own, which just compares each part of the GUID in a logical order. However you decide to compare GUIDs is up to you, but make sure that if a < b and b < c, you have a < c. Notice how I set a standard of triggering events. There are alternate ways. One way would be for the object triggering the event to create the event before calling the trigger event and deleting the event after calling the trigger event. Another way would be to create the event on the stack because the event itself is not abstract and pass the address on to the trigger function. The 3 main functions are all self-explanatory with their purpose.
Next, I needed an object. I chose to create a button that could be pushed by a person. When the button was pushed, it would spawn an event saying that it was pushed. To store all this data, a struct seemed like the logical choice.
typedef struct {
const char *who;
const char *bname;
} ButtonPushedEventData;
class __declspec(uuid("{0AE142DB-2B0F-45B4-8D24-A3390F7EC18E}")) ButtonPushedEvent : public Infinity::IEvent {
public:
ButtonPushedEvent(const char *_who, const char *_bname){
data.who = _who;
data.bname = _bname;
}
~ButtonPushedEvent(){}
INFINITY_GUID INFINITY_CALL GetEventType() const { return __uuidof(ButtonPushedEvent); }
INFINITY_VOID INFINITY_CALL GetEventData(INFINITY_VOID *pData){
memcpy(pData, &data, sizeof(data));
}
private:
ButtonPushedEventData data;
};
class CButton : public CEventManager {
public:
CButton(const char *_name) : name(_name) {}
~CButton() {}
void push(const char *who){
this->TriggerEvent(new ButtonPushedEvent(who, this->name));
}
private:
const char *name;
};
Now all we need are event handlers and we are done. I chose to create 2 so I could test removing one of them:
INFINITY_VOID INFINITY_CALL generic_bpress_handler(Infinity::IEvent *e){
if(e->GetEventType() == __uuidof(ButtonPushedEvent)){
ButtonPushedEventData data;
e->GetEventData(&data);
printf("Button \"%s\" pushed by \"%s\"\n", data.bname, data.who);
} else {
printf("We shouldn't be here...\n");
}
}
INFINITY_VOID INFINITY_CALL oops(Infinity::IEvent *e){
if(e->GetEventType() == __uuidof(ButtonPushedEvent)){
printf("HERPADERPADURR\n");
} else {
printf("We shouldn't be here 2...\n");
}
}
int main(int argc, char **argv){
CButton b1("lolwat"), b2("button");
b1.AttachEventListener(__uuidof(ButtonPushedEvent), generic_bpress_handler);
b2.AttachEventListener(__uuidof(ButtonPushedEvent), generic_bpress_handler);
b2.AttachEventListener(__uuidof(ButtonPushedEvent), oops);
b1.push("person");
b2.push("herp");
b2.DetachEventListener(__uuidof(ButtonPushedEvent), oops);
b2.push("herp");
system("pause");
return 0;
}
Aaaand here's an image of it in action:
You can currently find the source for this in the dev branch of its
Github repository under commit 01a4f93.
What do you consider to be the advantages of such an event distribution system compared to a signal/slot approach (like in libsigc++, Boost.Signals or sigslot)?
It seems to require a downcast (putting type safety into the hands of the user, any mistake would lead to a crash that is only detectable at runtime when the event gets fired). It also adds a map lookup to event processing that isn't present in the signal/slot design.
A possible advantage you could mention might be that a visual editor or scripting language binding could enumerate the available events (a feature easily implemented). Though of course one might still argue that decoupling such reflection abilities (by using a signal/slot system and an external reflection information provider class) would be truer to the SRP and allow seamless binding of third-party classes, too.
An unrelated note on the implementation:
Your CEventManager::TriggerEvent() deletes its argument, but the ownership transfer doesn't seem apparent from the outside. Or even required: a const ref would allow construction of the event arguments on the stack of the caller and dissolve any ownership concerns.