I'm interested in receiving some feedback regarding an Event system that I wrote. Both in style and implementation, but also in overall design and the design decisions that I made.
It is intended to be used as part of a game (hobby project of mine) and is intended to be used on a single thread. While receivers will always receive events on the services owning thread, I may add the ability for other threads to send events in the future. It is intended to have very low overhead in regards to execution time and scale well.
Here it is:
EventService.h
#pragma once
#include "Core\ITimerService.h"
#include "Events\EventListener.h"
#include "Helpers\NonCopyable.h"
#include <thread>
#include <chrono>
#include <functional>
#include <typeindex>
#include <unordered_map>
#include <memory>
namespace quasar
{
// The EventService provides a service for objects to
// register an interest in recieving events, which are arbitary typed
// data and an to send those events. Events are guaranteed to be
// executed at a known point of execution but in an undefined order.
//
// It is currently not thread safe.
//
class EventService : public NonCopyable
{
public:
template <typename EventT>
using OnEventSignature = void(const EventT& evnt);
// Constructor
// Intializes the EventService with the calling thread as the owning thread
// param timerService The timer service to use for timed events
EventService(ITimerService* timerService);
// Constructor
// Intializes the EventService
// param timerService The timer service to use for timed events
// param owningThread The thread that owns this event service
EventService(ITimerService* timerService, const std::thread::id& owningThread);
// Queues up an event for execution
// param evnt the event to queue up
template <typename EventT>
void send(EventT&& evnt);
// Queues up an event for execution after a delay
// param evnt the event to queue up
// param delay the time delay until the event should be
// exeucted, guaranteed to wait at least
// for the delay and execute on the very
// next executeEventQueue call afterwards
template <typename EventT>
void send(EventT&& evnt, std::chrono::duration<std::chrono::steady_clock> delay);
// Registers a Listener interested in handling events
// tparam EventT the exact type of event that the listener
// is interested in.
// tparam ListenerT the (exact) type of listener to register,
// will tie the ListenerT's
// void onEvent(const EventT&) method as the
// trigger function.
// param owner the listener, the lifetime of the registration
// will be tied to the lifetime of the listener
template <typename EventT, typename ListenerT>
void registerListener(ListenerT& owner);
// Registers a Listener interested in handling events
// tparam EventT the exact type of event that the listener
// is interested in.
// param owner the listener, the lifetime of the registration
// will be tied to the lifetime of the listener
// param onEventFunction the function to invoke when the expected
// event is executed
template <typename EventT>
void registerListener(EventListener& owner, const std::function<OnEventSignature<EventT>>& onEventFunction);
// Infroms the service that a listener is no longer
// interested in recieving events
// param handle the handle of the registration
// to unregister
// returns true if the listener was removed, false if it was
// not registered
bool unregisterListener(const RegisteredEventListenerHandle& handle);
// Executes all events in the event queue,
// as well as all delayed events whose time
// to execute has come
void executeEventQueue();
private:
class IEventTypeNode;
template <typename EventT>
class EventNode;
template <typename EventT>
EventNode<EventT>& getEventNode();
ITimerService* timerService;
std::thread::id owningThread;
std::unordered_map<std::type_index, std::unique_ptr<IEventTypeNode> > listenerMap;
uint32 lastListenerId;
};
}
#include "Events\EventService.hpp"
EventService.hpp
#pragma once
#include "Events\EventService.h"
#include <queue>
#include <utility>
namespace quasar
{
class EventService::IEventTypeNode
{
public:
virtual ~IEventTypeNode() = default;
template <typename EventT>
EventNode<EventT>& getAsEventTypeNode()
{
#ifdef _DEBUG
auto ptr = dynamic_cast<EventNode<EventT>*>(this);
assert(ptr != nullptr);
return *ptr;
#else
return *static_cast<EventNode<EventT>*>(this);
#endif // _DEBUG
}
virtual void removeListenerRegistration(uint32 id) = 0;
virtual void executeEventQueue(const std::chrono::time_point<std::chrono::steady_clock> currentTime) = 0;
};
template <typename EventT>
class EventService::EventNode : public EventService::IEventTypeNode
{
public:
using OnEventFuncT = std::function<EventService::OnEventSignature<EventT>>;
void queueUp(EventT&& evnt)
{
this->eventQueue.push_back(evnt);
}
void queueUp(EventT&& evnt, std::chrono::time_point<std::chrono::steady_clock> triggerPoint)
{
this->timedEventQueue.push(std::make_pair(std::forward(evnt), triggerPoint));
}
void addListenerRegistration(uint32 id,
const OnEventFuncT& onEventFunc)
{
assert(this->onEventFunctions.find(id) == this->onEventFunctions.end());
this->onEventFunctions[id] = onEventFunc;
}
void addListenerRegistration(uint32 id,
OnEventFuncT&& onEventFunc)
{
assert(this->onEventFunctions.find(id) == this->onEventFunctions.end());
this->onEventFunctions[id] = onEventFunc;
}
void removeListenerRegistration(uint32 id) override
{
assert(this->onEventFunctions.find(id) != this->onEventFunctions.end());
this->onEventFunctions.erase(this->onEventFunctions.find(id));
}
void executeEventQueue(const std::chrono::time_point<std::chrono::steady_clock> currentTime) override
{
for (const auto& function : this->onEventFunctions)
{
for (const EventT& evnt : this->eventQueue)
{
function.second(evnt);
}
}
this->eventQueue.clear();
while (!this->timedEventQueue.empty())
{
const TimedEventT& timedEvent = this->timedEventQueue.top();
if (currentTime >= timedEvent.second)
{
for (const auto& function : this->onEventFunctions)
{
function.second(timedEvent.first);
}
this->timedEventQueue.pop();
}
else
{
break;
}
}
}
private:
using TimedEventT = std::pair<EventT, std::chrono::time_point<std::chrono::steady_clock>>;
struct IsScheduledEarlier
{
bool operator()(const TimedEventT& left, const TimedEventT& right) const
{
return left.second < right.second;
}
};
std::vector<EventT> eventQueue;
std::priority_queue<TimedEventT, std::vector<TimedEventT>, IsScheduledEarlier> timedEventQueue;
std::unordered_map<uint32, OnEventFuncT> onEventFunctions;
};
template <typename EventT>
EventService::EventNode<EventT>& EventService::getEventNode()
{
std::type_index index(typeid(EventT));
auto found = this->listenerMap.find(index);
if (found == this->listenerMap.end())
{
auto newNode = std::make_unique<EventNode<EventT>>();
this->listenerMap[index] = std::move(newNode);
found = this->listenerMap.find(index);
}
return found->second->getAsEventTypeNode<EventT>();
}
template <typename EventT>
void EventService::send(EventT&& evnt)
{
assert(std::this_thread::get_id() == this->owningThread);
this->getEventNode<EventT>().queueUp(std::forward<EventT>(evnt));
}
template <typename EventT>
void EventService::send(EventT&& evnt, std::chrono::duration<std::chrono::steady_clock> delay)
{
assert(std::this_thread::get_id() == this->owningThread);
this->getEventNode<EventT>().queueUp(std::forward<EventT>(evnt), this->timerService->timestamp() + delay);
}
template <typename EventT, typename ListenerT>
void EventService::registerListener(ListenerT& owner)
{
using OnEventMemberType = void(ListenerT::*)(const EventT&);
this->registerListener<EventT>(owner,
std::bind(static_cast<OnEventMemberType>(&ListenerT::onEvent), &owner, std::placeholders::_1));
}
template <typename EventT>
void EventService::registerListener(EventListener& owner,
const std::function<OnEventSignature<EventT>>& onEventFunction)
{
assert(std::this_thread::get_id() == this->owningThread);
uint32 id = lastListenerId++;
this->getEventNode<EventT>().addListenerRegistration(id, onEventFunction);
owner.addRegistrationHandle(RegisteredEventListenerHandle(typeid(EventT), this, id));
}
}
EventService.cpp
#include "Events\EventService.h"
namespace quasar
{
EventService::EventService(ITimerService* timerService)
: EventService(timerService, std::this_thread::get_id())
{
}
EventService::EventService(ITimerService* timerService, const std::thread::id& owningThread)
: timerService(timerService)
, owningThread(owningThread)
, lastListenerId(1)
{
}
bool EventService::unregisterListener(const RegisteredEventListenerHandle& handle)
{
auto found = this->listenerMap.find(handle.typeIndex);
if (found != this->listenerMap.end())
{
found->second->removeListenerRegistration(handle.id);
return true;
}
return false;
}
void EventService::executeEventQueue()
{
auto timestamp = this->timerService->timestamp();
for (const auto& node : this->listenerMap)
{
node.second->executeEventQueue(timestamp);
}
}
}
EventListener.h
#pragma once
#include "Core\NumTypes.h"
#include "Helpers/NonCopyable.h"
#include <vector>
#include <typeindex>
namespace quasar
{
class EventService;
// Handle that identifies a uniqe event listener registration
struct RegisteredEventListenerHandle
{
RegisteredEventListenerHandle(const std::type_index& typeIndex,
EventService* eventService,
uint32 id);
std::type_index typeIndex;
EventService* eventService;
uint32 id;
};
//
// Base class for objects capable of listening for events
// Handles lifetime of listener registration
//
class EventListener : public NonCopyable
{
public:
virtual ~EventListener();
// Ties the lifetime of a listener regitration to this listener
// param handle the handle of the registration
void addRegistrationHandle(const RegisteredEventListenerHandle& handle);
private:
std::vector<RegisteredEventListenerHandle> handles;
};
}
EventListener.cpp
#include "Events\EventListener.h"
#include "Events\EventService.h"
namespace quasar
{
RegisteredEventListenerHandle::RegisteredEventListenerHandle(const std::type_index& typeIndex,
EventService* eventService,
uint32 id)
: typeIndex(typeIndex)
, eventService(eventService)
, id(id)
{
}
EventListener::~EventListener()
{
for (auto& handle : this->handles)
{
handle.eventService->unregisterListener(handle);
}
}
void EventListener::addRegistrationHandle(const RegisteredEventListenerHandle& handle)
{
this->handles.push_back(handle);
}
}