Introduction
I have just started learning templates and experimenting with function pointers. I wanted to create an event system that met a couple of goals:
- Event types are PODs, and do not inherit from a base 'Event' class
- External API is simple, no binding or passing in lambdas etc.
- No explicit registering of event types by user, any POD struct like object should work
The implementation currently works, however hasn't been heavily tested. The code will be used in my small "game engine" project, done purely as a learning exercise.
Focus
I am most interested in the way I am storing and calling the functions. I wanted a way to store any arbitrary event type that met the conditions described above.
I'm not convinced on the way I am handling the ListenerHandle
s which are solely used to enable the remove_listener
feature. However this is of less concern to me than my previous point.
Future
- Multithreading/Async
- Delayed event queues
Usage
I'll start with what it looks like to use the API, I am relatively happy with this aspect:
#include "EventManager.h"
const int key_w = 87;
struct CollisionEvent
{
int x;
int y;
};
struct KeyPressEvent
{
int key_code;
};
class InputHandler
{
public:
InputHandler()
{
EventManager::listen<KeyPressEvent>(this, &InputHandler::on_key_press);
}
private:
void on_key_press(KeyPressEvent* event)
{
// ...
}
};
void handle_collision(CollisionEvent* event)
{
// ...
}
int main()
{
InputHandler input_handler;
EventManager::fire<KeyPressEvent>(key_w);
ListenerHandle handle = EventManager::listen<CollisionEvent>(&handle_collision);
EventManager::fire<CollisionEvent>(10, 3);
EventManager::remove_listener(handle);
}
Implementation
It is implemented in a single header EventManager.h
, and is roughly ~140 lines without whitespace. I will divide each part into its own block for readability.
ListenerHandle
A ListenerHandle
is returned to the user upon registering an event callback. Each callback function is uniquely associated with one handle, allowing users to remove their callback.
#pragma once
#include <cassert>
#include <cstdint>
#include <vector>
#include <memory>
#include <functional>
#include <queue>
typedef std::uint32_t EventID;
class ListenerHandle
{
public:
ListenerHandle(std::uint32_t sparse_index = 0u, std::uint32_t dense_index = 0u, std::uint32_t version = 0u, EventID event_id = 0u)
: m_sparse_index(sparse_index),
m_dense_index(dense_index),
m_version(version),
m_event_id(event_id) {}
public:
std::uint32_t m_sparse_index;
std::uint32_t m_dense_index;
std::uint32_t m_version;
EventID m_event_id;
friend class EventManager;
template<typename T>
friend struct CallbackContainer;
};
CallbackContainer
A callback container is used to store each callback of the same event type (i.e from the example above, all KeyPressEvent
callbacks would be stored together in an instance of CallbackContainer
).
A sparse set is used to track which ListenerHandle
belongs to which callback. Each index in the sparse-set is versioned, so you can only remove the callback originally associated with your handle.
struct CallbackContainerBase
{
virtual void remove_callback(const ListenerHandle& handle) = 0;
};
template<typename T>
struct CallbackContainer : CallbackContainerBase
{
CallbackContainer(EventID event_id) : event_id{ event_id } {}
std::vector<std::function<void(T*)>> callbacks;
std::vector<ListenerHandle> handles;
EventID event_id;
std::vector<ListenerHandle> sparse;
std::vector<std::uint32_t> dense;
std::queue<std::uint32_t> free_sparse_indices;
template<typename T_Function>
auto add_callback(T_Function callback)->ListenerHandle;
void remove_callback(const ListenerHandle& handle) override;
};
template<typename T>
inline void CallbackContainer<T>::remove_callback(const ListenerHandle& handle)
{
assert(handle.m_version == sparse[handle.m_sparse_index].m_version);
sparse[handle.m_sparse_index].m_version++;
std::uint32_t remove_index = sparse[handle.m_sparse_index].m_dense_index;
std::uint32_t last_index = callbacks.size() - 1;
dense[remove_index] = dense[last_index];
sparse[dense[remove_index]].m_dense_index = remove_index;
std::swap(callbacks[remove_index], callbacks[last_index]);
free_sparse_indices.push(handle.m_sparse_index);
callbacks.pop_back();
dense.pop_back();
}
template<typename T>
template<typename T_Function>
inline auto CallbackContainer<T>::add_callback(T_Function callback) -> ListenerHandle
{
std::uint32_t sparse_index;
if (free_sparse_indices.empty())
{
sparse_index = callbacks.size();
sparse.emplace_back(sparse_index, sparse_index, 0u, event_id);
}
else
{
sparse_index = free_sparse_indices.front();
free_sparse_indices.pop();
}
dense.push_back(sparse_index);
callbacks.emplace_back(callback);
return sparse[sparse_index];
}
EventManager
This is the class that users interact with, a trivial system is used to create run-time available IDs to identify event types, which act as indexes into the vector of CallbackContainer
's.
Each CallbackContainer
is stored as its base CallbackContainerBase
to allow for the storage of any arbitrary type, and then casted to the correct type when needed.
class EventManager
{
using CallbackContainers = std::vector<std::unique_ptr<CallbackContainerBase>>;
public:
template<typename T, typename T_Function>
static auto listen(T_Function callback)->ListenerHandle;
template<typename T, typename T_Instance, typename T_Function>
static auto listen(T_Instance* instance, T_Function callback)->ListenerHandle;
static void remove_listener(const ListenerHandle& handle);
template<typename T, typename... T_Args>
static void fire(T_Args...args);
private:
template<typename T>
static auto get_event_id()->EventID;
template<typename T>
static auto register_event()->EventID;
private:
static inline CallbackContainers s_callbacks;
static inline EventID s_next_event_id{ 0u };
};
template<typename T, typename T_Function>
inline ListenerHandle EventManager::listen(T_Function callback)
{
return static_cast<CallbackContainer<T>*>(s_callbacks[get_event_id<T>()].get())->add_callback(callback);
}
template<typename T, typename T_Instance, typename T_Function>
inline ListenerHandle EventManager::listen(T_Instance* instance, T_Function callback)
{
return static_cast<CallbackContainer<T>*>(s_callbacks[get_event_id<T>()].get())->add_callback([instance, callback](T* event) { (instance->*callback)(event); });
}
inline void EventManager::remove_listener(const ListenerHandle& handle)
{
s_callbacks[handle.m_event_id]->remove_callback(handle);
}
template<typename T, typename ...T_Args>
inline void EventManager::fire(T_Args ...args)
{
T event{ args... };
auto& callbacks = static_cast<CallbackContainer<T>*>(s_callbacks[get_event_id<T>()].get())->callbacks;
for (auto& callback : callbacks)
callback(&event);
}
template<typename T>
inline EventID EventManager::get_event_id()
{
static EventID event_id = register_event<T>();
return event_id;
}
template<typename T>
inline EventID EventManager::register_event()
{
s_callbacks.emplace_back(std::make_unique<CallbackContainer<T*>>(s_next_event_id));
return s_next_event_id++;
}
Thank you for your time, it is greatly appreciated!
1 Answer 1
No need for s_callbacks[]
You have a vector of pointers to CallbackContainerBase
. But to get the index into that, you have get_event_id()
, which creates a unique ID once, and stores it in a static
local variable event_id
. But you can do exactly the same, not with an EventID
, but with a CallbackContainer
. Consider:
template<typename T>
inline CallbackContainer<T>& EventManager::get_callbacks()
{
static CallbackContainer<T> callbacks{}; // no need for an ID anymore
return callbacks;
}
But you can also use inline
variables, which you are already using, in combination with templates, so you can write:
class EventManager
{
template<typename T>
static inline CallbackContainer<T> s_callbacks;
...
};
This greatly simplifies the rest of your code; for example:
template<typename T, typename T_Function>
inline ListenerHandle EventManager::listen(T_Function callback)
{
return s_callbacks<T>.add_callback(callback);
}
Simplify the callback container
Your CallbackContainer
class is quite complicated. It has five containers. How many callbacks do you think you need to store before the overhead of this is worth it? Consider that the size of an empty std::function<>
is typically 32 bytes on 64-bit machines, and your ListernerHandle
itself is already 16 bytes.
The individual indices in a handle are only 32 bits. Ideally the code registering a listener only needs a handle that is 32 bits to uniquely identify the listener.
Here is another approach that is simpler, can still handle lots of callbacks, has \$O(1)\$ insertion and removal, but is not sparse:
typename<typename T>
struct CallbackContainer
{
struct ListenerHandle
{
std::uint32_t id;
};
std::vector<std::function<void(T&)> callbacks;
std::vector<ListenerHandle> free_handles;
template<typename T_Function>
auto add_callback(T_Function callback) -> ListenerHandle;
void remove_callback(ListenerHandle handle) override;
};
template<typename T>
template<typename T_Function>
inline auto CallbackContainer<T>::add_callback(T_Function callback) -> ListenerHandle
{
ListenerHandle handle;
if (free_handles.empty())
{
handle = callbacks.size();
callbacks.emplace_back(handle);
}
else
{
handle = free_handles.back();
free_handles.pop_back();
callbacks[handle] = callback;
}
return handle;
}
template<typename T>
void CallbackContainer<T>::remove_callback(ListenerHandle handle)
{
callbacks[handle] = nullptr;
free_handles.push_back(handle);
}
It just requires a slight modification when firing events:
template<typename T, typename ...T_Args>
inline void EventManager::fire(T_Args ...args)
{
T event{ args... };
for (auto& callback : s_callbacks<T>.callbacks)
if (callback)
callback(event);
}
Also note that I moved the declaration of ListenerHandle
into CallbackContainer
. This makes ListernerHandle
implicitly templated on T
as well, adding more type safety. It does require that you pass the template type to EventManager::remove_listener
though:
template<typename T>
inline void EventManager::remove_listener(CallbackContainer<T>::ListenerHandle handle)
{
s_callbacks<T>.remove_callback(handle);
}
But where you are calling remove_listener()
you probably don't need to make any changes, as T
can be deduced from the handler passed as the parameter.
There are other tradeoffs that can be made; you can keep callbacks
dense and still avoid most of the other containers, but then adding and/or removing might not be possible in \$O(1)\$ time anymore. For example:
typename<typename T>
struct CallbackContainer
{
...
std::vector<std::function<void(T&)> callbacks;
std::vector<ListenerHandle> handles;
...
};
typename<typename T>
typename<typename T_Function>
inline auto CallbackContainer<T>::add_callback(T_Function callback) -> ListenerHandle
{
static ListenerHandle next_handle;
do {
++next_handle.id;
} while (std::find(handles.begin(), handles.end(), next_handle) != handles.end());
callbacks.push_back(callback);
handles.push_back(next_handle);
return next_handle;
}
template<typename T>
void CallbackContainer<T>::remove_callback(ListenerHandle handle)
{
auto pos = std::find(handles.begin(), handles.end(), handle) - handles.begin();
std::swap(handles[pos], handles.back());
handles.pop_back();
std::swap(callbacks.pos[], callbacks.back());
callbacks.pop_back();
}
Creating the event
object
When you call fire()
, you construct a T event
first, using the arguments passed to fire()
. However, you take all arguments by value, but it is usually better to use perfect forwarding, like so:
template<typename T, typename ...T_Args>
inline void EventManager::fire(T_Args&& ...args)
{
const T event(std::forward<T_Args>(args)...);
...
}
This allows proper forwarding of references and also allows for move-construction of event
. Note that I also made it const
here, as you might not want one callback to modify the event data when another callback still needs to be fired.
Note that in addition to that version of fire()
, you can add an additional overload that takes a const
reference to a T
:
template<typename T>
inline void EventManager::fire(const T& event)
{
...
}
This overload will allow T
to be deduced when you pass it an existing event object:
CollisionEvent collision_event(10, 3);
EventManager::fire(collision_event);
Avoid unnecessary use of friend
Using friend
is a code smell; there is a good chance that code using it has too much coupling between different classes. Sometimes it is necessary, but in other cases it is unnecessary and can be avoided.
I don't see why ListenerHandle
has to be a friend
of EventManager
and CallbackContainer<T>
; everything it has is public
.
Add a fire()
function to CallbackContainer
EventManager
has to be able to access CallbackContainer<T>::callbacks
in order for fire()
to work. At the moment, CallbackContainer
is a struct
where everything is public
. However, ideally you would make this a class
, so it can hide its implementation details. You do need to fire events though, so to avoid having to add a friend EventManager
, I would add a public
fire()
member function to CallbackContainer
, which EventManager
can then call.