I'm considering different approaches to implementing events in a C++ application. There's a suggestion to implement a centralized event dispatch, via a notification center. An alternative would be for sources and targets of the events to communicate directly. I'm having reservations about the notification center approach, however. I'll outline both approaches as I see them (I could well be misunderstanding something about them, I've never implemented event handling before).
a) Direct communication. Events are a part of their source's interface. Objects interested in the event must somehow get a hold of an instance of the source class and subscribe to its event(s):
struct Source
{
Event</*some_args_here*/> InterestingEventA;
Event</*some_other_args_here*/> InterestingEventB;
};
class Target
{
public:
void Subscribe(Source& s)
{
s.InterestingEventA += CreateDelegate(&MyHandlerFunction, this);
}
private:
void MyHandlerFunction(/*args*/) { /*whatever*/ }
};
(From what I understand, boost::signals, Qt signals/slots and .NET events all work like this, but I could be wrong.)
b) Notification center. Events aren't visible in their source's interface. All events go to some notification center, probably implemented as a singleton (any advice on avoiding this would be appreciated), as they are fired. Target objects don't have to know anything about the sources; they subscribe to certain event types by accessing the notification center. Once the notification center receives a new event, it notifies all its subscribers interested in that particular event.
class NotificationCenter
{
public:
NotificationCenter& Instance();
void Subscribe(IEvent& event, IEventTarget& target);
void Unsubscribe(IEvent& event, IEventTarget& target);
void FireEvent(IEvent& event);
};
class Source
{
void SomePrivateFunc()
{
// ...
InterestingEventA event(/* some args here*/);
NotificationCenter::Instance().FireEvent(event);
// ...
}
};
class Target : public IEventTarget
{
public:
Target()
{
NotificationCenter::Instance().Subscribe(InterestingEventA(), *this);
}
void OnEvent(IEvent& event) override {/**/}
};
(I took the term "Notification center" from Poco, which, as far as I understand, implements both approaches).
I can see some pros to this approach; it will be easier for the targets to create their subscriptions because they wouldn't need access to the sources. Also, there wouldn't be any lifetime management problems: unlike sources, the notification center will always outlive the targets, so they targets always unsubscribe in their destructors without worrying whether the source still exists (that's a major con I can see in the direct communication). However, I am afraid that this approach could lead to unmaintainable code, because:
All sorts of events, probably completely unrelated to each other, would go to this one big sink.
The most obvious way of implementing the notification center is as a singleton, so it will be hard to track who and when modifies the subscribers' list.
Events aren't visible in any interface, so there is no way to see whether a particular event belongs to any source at all.
As a result of these cons, I'm afraid that as the application grows it would become very difficult to track connections between objects (I'm imagining problems trying to understand why some particular event doesn't fire, for example).
I'm looking for advice regarding the pros and cons of the "notification center" approach. Is it maintainable? Does it fit every sort of applications? Maybe there are ways to improve the implementation? Comparison between the two approaches I described, as well as any other event handling suggestions, are most welcome.
These approaches are orthogonal. Direct communication should be used when the events are exchanged between specific objects. The notification center approach should only be used for broadcast events, e.g. when you want to process all events of a given type regardless of their source, or when you want to send an event to some set of objects which you don't know in advance.
To avoid singletons, reuse the code for direct communication and subscribe the notification center object to all events you want to process in this way. To keep things manageable, you would do this in the emitting objects.
The lifetime-related problems with direct communication are usually solved by requiring that every class that subscribes to any events must be derived from a specific base class; in Boost.Signals, this class is called trackable
. The equivalent of your CreateDelegate
function stores the information about subscriptions for the given object in data member within trackable
. On destruction of trackable
, all subscriptions are cancelled by calling the matching Unsubscribe
functions. Note that this is not thread-safe, because the destructor of trackable
is called only after the derived class destructor finishes - there is a period where a partially destroyed object can still receive events.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With