Generic Observer framework in C++17
Sometime ago I wanted to use the Observer design pattern, to emit events from an event broker (subject) to multiple event handlers (observers). I wanted to implement different events as their own classes or structs. I wanted the event handlers to be able to handle multiple events of different types. I did not want the Events themselves to contain any information pertaining to the type of the class/struct (no enumeration indicating the type of the event). When the Event Handler would receive an Event, the correct method for that Event should be called via method overloading. To summarize I want to dispatch received Events (dynamically or statically) to the fitting method.
I also wanted my Observer framework to be a standalone module that used generics so it could be reusable for different types.
The Observer framework
For my observer framework I decided on the following design:
If you are familiar with the observer design pattern than this is nothing really special. If you where to implement this UML class diagram in C++ (in a single header file). You would get the following:
#ifndef OBSERVER_HPP #define OBSERVER_HPP #include <list> #include <memory> /* PS: This is a header only implementation of the above class diagram. * If you are planning on using this for a personal project you should split it up, * into different header and source files, for the sake of cleanness. */ namespace observer { // Forward Declarations: template<typename T> class Subject; // Classes: template<typename T> class Observer { private: Subject<T>* m_subject; protected: auto subject() const -> Subject<T>* { return m_subject; } public: Observer(const Subject<T>* t_subject) : m_subject{t_subject} {} virtual auto notify(T&& t_notification) -> void = 0; virtual ~Observer() = default; }; // Aliases: template<typename T> using ObserverPtr = std::shared_ptr<Observer<T>>; // Classes: template<typename T> class Subject { private: std::list<ObserverPtr<T>> m_observers; public: auto subscribe(ObserverPtr<T> t_observer) -> void { m_observers.push_back(std::move(t_observer)); } auto unsubscribe(ObserverPtr<T> t_observer) -> void { m_observers.remove(std::move(t_observer)); } auto notify(T&& t_notification) -> void { for(auto& observer : m_observers) { observer->notify(std::forward<T>(t_notification)); } } virtual ~Subject() = default; }; } // namespace observer #endif // OBSERVER_HPP
This implementation would work for multiple types. Furthermore since this framework has the added bonus of using templates meaning that this observer package is not be dependent on any other type or class, until you instantiate it. Which means this observer package will only have dependencies to it not from it (that is why I call this a framework).
Using dynamic dispatch
But what I am more interested in is using this Observer implementation in the following way:
Here we instantiate the the observer framework on an Event
pointer.
The events are inherit from the Event
class.
Then in the event handlers we will use dynamic dispatch to call the correct method, to handle the Event
.
We would implement EventHandler
like this:
#ifndef EVENT_HANDLER_HPP #define EVENT_HANDLER_HPP #include "events.hpp" #include "observer.hpp" namespace handler { // Aliases (This should really be defined somewhere in the events package): using EventSubject = observer::Subject<events::Event*>; using EventObserver = observer::Observer<events::Event*>; // Classes: class EventHandler : public EventObserver { protected: auto notify(events::Event* t_event) -> void override { notify(t_event); } public: EventHandler(EventSubject* t_subject) : EventObserver{t_subject} {} auto notify(events::SomeEvent* t_event) -> void { // Handle SomeEvent } auto notify(events::AnotherEvent* t_event) -> void { // Handle AnotherEvent } }; } // namespace handler #endif // EVENT_HANDLER_HPP
Except this does not work!
The auto EventHandler::notify(Event* t_event) -> void
method will recursively call itself, until the program crashes.
This happens because the base class observer::Observer<T>
does not have an overload for any of the other Event
types.
In C++ when you use dynamic dispatch it first looks for viable candidates in the vtable of the base class.
In this case it finds a viable candidate auto Observer<Event*>::notify(Event* t_event) -> void
it then resolves to our overriden method of auto EventHandler::notify(Event* t_event) -> void
.
Causing it to recursively call itself over and over until it finally crashes.
You could solve this by using a dynamic_cast
but this is not really a scalable solution.
Static dispatch to the rescue
So we cannot use dynamic dispatching to achieve this behavior.
Luckily we can achieve the desired behavior by using std::variant
.
First lets go back to the drawing table and do a redesign:
You might notice that we have substituted the Event
class with the EventVariant
class.
This EventVariant
is not actually a class but an alias (UML specification does not include aliases):
namespace events { // Aliases: using EventVariant = std::variant<SomeEvent, AnotherEvent>; } // namespace events
We could now reimplement the EventHandler
class as the following:
#ifndef EVENT_HANDLER_HPP #define EVENT_HANDLER_HPP #include "events.hpp" #include "observer.hpp" namespace handler { // Aliases (This should really be defined somewhere in the events package): using EventSubject = observer::Subject<events::EventVariant>; using EventObserver = observer::Observer<events::EventVariant>; // Classes: class EventHandler : public EventObserver { protected: auto notify(events::EventVariant t_event) -> void override { std::visit([this](auto&& t_event) { this->notify(t_event); } , t_event); } public: EventHandler(EventSubject* t_subject) : EventObserver{t_subject} {} auto notify(events::SomeEvent t_event) -> void { // Handle SomeEvent } auto notify(events::AnotherEvent t_event) -> void { // Handle AnotherEvent } }; } // namespace handler #endif // EVENT_HANDLER_HPP
And this will now work.
std::visit
will now properly resolve the type for us causing the proper method to be called, this keeps the code nice and clean.
Using the Observer
You could now use the observer:
#include "observer.hpp" #include "events.hpp" int main() { using namespace events; using namespace observer; using namespace broker; using namespace handler; // Define the Subject and Observer EventBroker broker; auto handler{std::make_shared<EventHandler>(&broker)}; // Subscribe the Event Handler to the broker broker.subscribe(handler); // Notify the event handler broker.notify(SomeEvent{}); broker.notify(AnotherEvent{}); }
Lastly I use PlantUML to generate beautiful UML diagrams.
Not a perfect solution
While this solution works its not exactly perfect.
If you payed attention than you would see that the EventVariant
is now dependent on every event that you woul want to dispatch.
Meaning that if you add an event you have to manually add your event class to the EventVariant
alias.
This is a minor price to pay for the convenience it offers