Sign in to follow this  

Clean Event Listener

Recommended Posts

Hello forum : )
 

I took many tips and suggestions into consideration in order to change my code: Less templates, avoiding multi-inheritance, ...

Nonetheless, I struggle to plug-in my last core feature of my event listener-module, passing variables from e.g. the input-module to the window-module.

Reason: When my event-caller iterates over a vector of listening functions, I cannot pass the specific event-type without static casting at some point.

 

To clarify a few things:

I will call a class that wants to listen to a certain amount of events Listener or as a representative example: Window-class.

The module that owns all registered listeners and calls them will be called Caller.

A specific event (as the click-event) derives from the base-event-class.

 

My dream:

A listener shall have a clean structure: It defines an on_event()-function with an overloaded signature per possible event-type.

Example: on_event(click_event& event_object) and on_event(resize_event& event_object)

When the potential listener (e.g. window-class) gets constructed, it shall ask the caller to register it.

First, the listener calls a member-function, just to make things look prettier, as an add_listener with a ton of parameters becomes quickly bloated and ugly.

So I said, it shall be a template-function doing this, add_listener<click_event>(); - this one calls the caller eventually; providing needed additional information (lifetime, ..).

I actually just want only one add_listener-function for all event_types. That is another important philosophy I try to get done.

I want my event-system to make my code slick and not bloating it while growing event-types.

 

My caller-class just identifies what type of event shall be registered. I want my caller to be independent, no new code whenever a new event-type gets added. That would be great.

That is why every event-type is deriving from a base.

I have an unordered map: Keys are all possible event-types (std::type_index) and values are vectors of Registration-objects.

 

A registration-object is representing a listener. When an event gets triggered, e.g. from an input-module who recognised a mouse click, will pass a click-event to caller.

The caller identifies the type and opens the vector of such registration-objects.

 

My issue:

Having on_event() with overloaded possibilities just feels impossible for me.

I tried using std::function<void(T&)> from within the template-function add_listener<T>(), but then I would have to pass that to my caller-class.

And my compiler will complain about T& cannot be turned into the base-event-class. I do not like to spam templates along my design either.

 

Alternatively, I to do no overloaded event-callbacks, e.g. on_click_event(event_base&) and on_resize_event(event_base&).

But this would require me to perform a static cast within those functions, which is weird.

 

I tried a lot and nothing really feels like clean design.

My last hope was to create a unique vector for each event-type, but that would require modifying the event-caller whenever I want to add a new type.

If I have 20 events, I would have to create 20 vectors at some point, which is ugly too.

 

It would be awesome, if the only thing I need to do per new type, is to add a new Event-Callback-function and a new class for this event-type.

I wish I could avoid static casting too.

 

This is driving me crazy, isn't there a clean solution for this?

 

 

 

 

 

 

 

Share this post


Link to post
Share on other sites

I didn't follow previous discussions, so I may be suggesting things that were already discussed.

 

From your post, it looks to me like you want the impossible.

Basically, there are two possible solutions as far as I can see (for storing listeners for each broadcast object separately):

 

- Setup a listener administration for each event type separately.

- Throw all listeners together in one big basket.

 

The first option breaks your "no new code for a new event" requirement, the second option breaks your "no static cast" requirement.

To elaborate, each event type separately obviously means you have to add a new administration for each new event type (or at the very least you need to list the set of available events somewhere). One big basket means at some point you need to cast from your generic Event to SpecializedEvent while broadcasting, to differentiate for each event type.

 

Another option could be to store listeners on a specific event-type together, for all broadcast objects together. However, that implies the broadcast objects need to be unified such that they can be thrown into one basket. This can be as simple as a unique number, but whatever the form, such a unique ID needs to be retrieved while subscribing, which may not be nice. You also again get the problem of having to create a storage object for all subscriptions of a given type of event, which again breaks "no new code". This solution seems worse to me than separate storage for each broadcaster that was considered above.

 

 

In all, it seems to me that there is no way to fulfill all requirements, you'll have to drop at least one of them.

 

Personally, I see no advantage in the "big basket" solutions, different event listeners have pretty much nothing in common (on first sight), so I'd make an object that stores subscriptions for one type of event for one broadcaster. Such a broadcaster will have several such objects if it broadcasts several types of events. I would give external access to such an object so listeners can subscribe directly at such an object, but ymmv.

 

Share this post


Link to post
Share on other sites
Many code bases I've worked with have had something like this for the class users:

Messenger::AddListener( EventId, myCallback); // when Event ID integer event triggers, my function will be called
Messenger::AddListener<TypeA*> (EventID, myCallback ); // When Event ID integer event happens, my callback will be called with a single parameter of TypeA
Messenger::AddListener< TypeA*, TypeB*> (EventID, myCallback ); // Same thing, two parameters.
etc.

There are similar RemoveListener calls.

On the other side:

Messenger::Invoke( EventID ); // Basic event happened.
Messenger::Invoke<TypeA*>( EventID, a); // Invokes both the call with TypeA, and the basic call for the event.
Messenger::Invoke<TypeA*, TypeB*>(EventID, &a, &b); // Invokes for callback(a,b), callback(a), and callback() depending on what is registered.

Those specific functions invoke a well-known instance, but the class allows creating a private messenger instance. (The Messenger class is not a Singleton.)

The most difficult part, which really isn't that difficult, is to provide a member function or lambda for the callback. Thankfully there are standard library functions to make member function calls so the process is fairy simple.

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

Sign in to follow this