Sign in to follow this  
Angelic Ice

Inheriting from multiple Events

Recommended Posts

Hello forum!

 

I wanted to know if the following kind of multiple inheritance is ok.

 

Let us imagine a caller system. Objects of the application can listen to certain events that happened.

Every event is being represented by an object residing in a map of the caller-object.

 

Every object in the application can let an event happen. Once that is the cause, everyone who inherited from that particular event will be called.

 

Example:

A window-class can inherit from the exit-button-clicked- and the resized-event.

As a result, it would inherit from multiple sources. While both events derive from the same class, there is no diamond-problem.

Every event pursues transmitting a unique communication between major application modules or minor game elements.

 

Nonetheless, I'm not really inheriting a skeleton but just a few unique functions for each listener, which can be called from the caller.

 

I thought about composition but it was not looking as straight forward. Got the idea of something with shared pointers... but how to inform listeners if all they do, is holding a pointer to an event-object... iterating over their event-objects and checking if certain things have been marked triggered? Ew.

 

In the end, I would like to know if my usage of multiple inheritance is bad?

 

 

Share this post


Link to post
Share on other sites

A window-class can inherit from the exit-button-clicked- and the resized-event.
This sounds wrong. A Window class is a window, it's not an event.

 

I could live with inheritance from an EventListener though.

 

 

What I am failing to see is how your event arrives with the interested objects

class Window: public MousePressEventListener {
    // Window implementation.
};

// Make two window objects.
Window win1;
Window win2;

Event e = new MousePressEvent(mouse_x, mouse_y); // Make an event of some type.
// Here I fail to see how 'e' arrives at 'win1' and 'win2'.

In the end, I would like to know if my usage of multiple inheritance is bad?
You use it as an interface, which is a good form I think.

For inspiration for how you can do this, have a look at a Java Gui toolkit. Objects interested in a certain type of event implement a handler interface (much like your base class), and subscribe themselves with an event source. When an event happens, it is send to all subscribed objects.

 

The advantage here is that you can have several event sources of the same type of event (if you need it), and objects can subscribe and unsubscribe dynamically.

Share this post


Link to post
Share on other sites

No, my window-class is still a window class. It just derives an on-event-function from an event-class, so the caller can call it.

Literally like your example.

 

Let's say we have an input-module. 

Now the user uses the left mouse button, the input-module registers this.

It creates a new mouse-press-event, just as in your example. Then, the input-module calls the caller-system's trigger-function - also passing our mouse-press-event.

The caller system looks in its map for registered listeners to the type mouse-press-event. Next step would be the caller calling an on-event-function of all listeners.

 

By the way, in my example, the listeners would obtain a smart-pointer upon registering in order to listen, that they can destroy in order to stop listening.

Edited by Angelic Ice

Share this post


Link to post
Share on other sites

from an event-class
I think I got confused here. A "event-class" is something like "class MousePressEvent" to me, while a "MousePressListener" is a event-listener class to me. You do use "listeners" a bit further in the text, which led me to the conclusion that the "event-class" was not a listener.

 

Otherwise we seem to agree.

Share this post


Link to post
Share on other sites

No, my window-class is still a window class. It just derives an on-event-function from an event-class, so the caller can call it. Literally like your example.


Just be careful about how you think about this problem. Inheritance should be translated to English as "is a". As in, if Window inherits from EventFoo, then "Window is an EventFoo."

This is more than just taxonomy, too. Remember that inheritance implies implicit up-casting and potential "object slicing" problems.

That's not to say that inheritance is wrong in this case specifically, but it should be thought through carefully.

For example, make very sure you're thinking through how to disable object slicing, e.g. make sure your EventEmitter has protected copy/move operations and destructor.
 
template <typename Event>
class EventEmitter {
protected:
  ~EventEmitter() = default;
  EventEmitter(EventEmitter const&) = default;
  EventEmitter(EventEmitter&&) = default;
  EventEmitter& operator=(EventEmitter const&) = default;
  EventEmitter& operator=(EventEmitter&&) = default;

By the way, in my example, the listeners would obtain a smart-pointer upon registering in order to listen, that they can destroy in order to stop listening.


I've seen this approach go awfully wrong, so be careful. Our engine at work uses a similar pattern and calls these smart pointers "receipts" which is cute I guess. If you go this route, at least make sure that these smart pointers are of a common type independent of the event type, so a caller can do something like:
 
vector<EventRecept> receipts;

receipts.push_back(window.listen<ClickEvent>(this, &MyClass::onClick));
receipts.push_back(window.listen<ScrollEvent>(this, &MyClass::onScroll));
// 12 other receipts
receipts.push_back(something.listen<AnotherEvent>(this, &MyClass::onAnother));
receipts.push_back(foo.listen<BarEvent>(this, &MyClass::onBar));

Otherwise your classes turn into a mess of event listener handle objects. That said, I rather despise the above pattern as well. It's insane to me to have to hold onto 20 distinct objects when the concept you're modeling is "clean up stuff when a lifetime ends."

For this reason, I very strongly prefer a lifetime handle approach. This is just an inversion of the above pattern: instead of _receiving_ a smart object when you register a handler, you _provide_ a smart object when you register a handler. Multiple handlers can be attached to the same smart object. Something like so:
 
Lifetime life;

window.listen<ClickEvent>(life, this, &MyClass::onClick);
If you have a class that has multiple lifetimes (e.g., a game state that knows the lifetime of the game session and the lifetime of a level) then it can have multiple Lifetime handles, but you only typically need one Lifetime whether you're registering for 1 or 500 things. Makes you objects smaller and more manageable.

Internally, the Lifetime can just be an object that holds a container of individual listener handles just like your original design, or the listeners can use the Lifetime to detect liveness. I've been leaning towards the later, giving us something like the following (using standard library types):
 
class Lifetime {
  shared_ptr<int> _shared;
public:
  weak_ptr<int> acquire() {
    if (!_shared)
      _shared = make_shared<int>(0);
    return _shared;
  }

  void reset() {
    _shared.reset();
  }
};

template <typename Event>
class Emitter {
  struct Listener {
    weak_ptr<int> handle;
    std::function<void(Event const&)> callback;
  };

  vector<Listener> _listeners;

public:
  void listen(Lifetime& life, std::function<void(Event const&)> callback) {
    _listeners.emplace_back(life.acquire(), move(callback));
  }

  void emit(Event const& event) {
    for (auto& listener: _listeners)
      if (listener.handle)
        listener.callback(event);

    _records.erase(remove_if(begin(_listeners), end(_listeners), [](auto& listener){ return !listener.handle; }), end(_listeners));
  }
};
The result is that when Lifetime is destroyed or manually reset, all the listeners that were registered using that lifetime will become inert. They won't be cleaned up immediately but will instead be "garbage collected" later when the event is actually fired, but I haven't seen this ever be a problem; it would only be so if you registered many listeners on an event that never fired. Worst possible non-hypothetical problem I can see is if your wrapper std::function captured some shared ownership handle and kept the object alive too long, but that's why you should avoid shared pointers in public interfaces at all costs (note how its used only as an internal implementation detail in my sample).

The further bonus of the above system: much faster cleanup. With the handle-per-listen approach, if you hold onto N handles and then cleanup (say, each game object has 5 and you just destroyed a level with 100 game objects, giving you 500 handles!), you'll force the system to cleanup/deregister N listeners (likely invoking some kind of O(N) behavior each time, giving you O(NlogN) cleanup time!). With the lifetime handle system, your cleanup is effectively O(N). Of course, big-O can be misleading, so depending on exact use case you might find one better than the other in terms of absolute performance, but neither approach is ideal if perf is the most important thing for you. :)

Share this post


Link to post
Share on other sites
Just be careful about how you think about this problem. Inheritance should be translated to English as "is a". As in, if Window inherits from EventFoo, then "Window is an EventFoo."

 

 

If you go this route, at least make sure that these smart pointers are of a common type independent of the event type, so a caller can do something like

 

I do not derive from the event-classes themselves, but a handler with template type for each event. Forgot to mention that, sorry : /

 

The lifetime-approach sounds interesting. Especially since I just started implementing the event-bus' use into my modules.

 

Let me try to grasp your approach into my own words:
Instead of returning a handler-object from the caller, I shall give my caller a lifetime-concept-object.

 

What I have at the time:

A window-class says, hey, I want to listen to every resize-event.

So it calls the handler-registration-method within the caller. Template type would be resize-event.

The caller looks for a container of the resize-event and add the listener to it.

And return a smart-object. Upon destruction of this smart-object, listening stops.

 

According to what I understood from your post:

A window-class calls the handler-registration-method within the caller. Same template-type but also providing a lifetime-object.

 

What we are trying to solve is the hassle of having one handler-class for each event-type our window-class wants to listen to, right?

If our lifetime-object shall not be just the storage for the event-handlers, would the caller keep the handlers/registrations and check if they lead to lifetime-objects that still exist whenever they would be iterated over upon event triggering?

 

This would remove the possibility to opt-out specific events whenever a listener wants to, right?

Let us say, an object only wants to listen to an event for once, it would still receive such events, as its lifetime-object is still alive.

This means, I would need a method within my caller that can be called by a listener, telling what template-type they do not want to receive messages anymore from, right?

 

By the way, why does your shared pointer use ints?

 

Thanks a lot for your valuable insight!
 

Edited by Angelic Ice

Share this post


Link to post
Share on other sites
Have you considered a signal-slot architecture? It's what Qt uses, so I am sure it works well for GUIs.

There is an implementation in Boost.Signals2 that I have used in the past and worked just fine for me. gtkmm (a C++ wrapper for GTK+) uses some other library, I think because of performance issues; but I wouldn't worry about it too much. Edited by Álvaro

Share this post


Link to post
Share on other sites

hat we are trying to solve is the hassle of having one handler-class for each event-type our window-class wants to listen to, right?


No, because there's no reason to have that problem in the first place. Use std::function.

The specific code I presented has some flaws that makes it look a little more complex than necessary and wouldn't compile; sorry, it was from-memory example code. :)
 

If our lifetime-object shall not be just the storage for the event-handlers, would the caller keep the handlers/registrations and check if they lead to lifetime-objects that still exist whenever they would be iterated over upon event triggering?


In the approach I presented, yes, but you could do it the other way too. I outlined why I liked my implementation, but others are also valid.
 

This would remove the possibility to opt-out specific events whenever a listener wants to, right?


Not at all. That's why the .reset() method on Lifetime. You can clear a Lifetime and _all_ associated handlers when you want. You are free to have as many Lifetime's as you want, since they're just objects. Unlike the shared-pointer-per-handler though you aren't force to manually track dozens of handlers if they all share the same relevant lifetime.
 

Let us say, an object only wants to listen to an event for once, it would still receive such events, as its lifetime-object is still alive.


If you code it that way. You can also make listeners that only receive an event once. This is common in many frameworks:
 
class EventEmitter {
  // as usual
  void listen(Lifetime& lifetime, std::function<void(Event&)> callback);

  // registers a listener that de-registers itself after activation
  void once(Lifetime& lifetime, std::function<void(Event&)> callback);
};

This means, I would need a method within my caller that can be called by a listener, telling what template-type they do not want to receive messages anymore from, right?


I don't think I understand the question.

You're free to make any kind of restrictions you want, but I _very very highly_ recommend just using std::function with some optional convenience routines. Your system should work with bound member functions, free functions, functors, and C++11 lambdas. This is exactly what std::function does for you.
 

By the way, why does your shared pointer use ints?


In this example, I'm basically "abusing" shared_ptr/weak_ptr for their lifetime-tracking semantics, and don't care about the value. Since shared_ptr<void> is illegal I used shared_ptr<int>.

A superior alternative would be to use a string or special object that lets you "name" your lifetimes. This would allow you to easily inspect an Emitter's list of registered handlers and see who your listeners are. e.g., something like:
 
class GameObject {
  Lifetime _self;
  Lifetime _level;
  string _name;
  ...
};

GameObject::GameObject(int id) :
  _name(format("GameObject(%d)", id)),
  _self(_name),
  _level(format("EnabledInLevel:%s", _name))
{
  // if you inspect manager, you'll see a listener for Event that's named "GameObject(1234)" or the like, which is handy
  manager.listen<Event>(_self, onEventHandler);
}

void GameObject::enabled(Level& level) {
  // if you inspect the level, you'll see a listener for AnotherEvent named "EnabledInLevel:GameObject(1234)"
  level.listen<AnotherEvent>(_level, onAnotherEventHandler);
}

Share this post


Link to post
Share on other sites

Thanks a lot : )

 

I thought a bit more about it and "lifetime" seems to be an odd name for it.

Doesn't it semantically indicate the lifetime of the listener? But then, all it does is telling the caller if the object is still able/interested to listen.

If I'm right, being unable to listen (listener has been destroyed) would need no lifetime-object, as the caller could just check the pointer, right?

The reset-method feels like I make the caller forget about the existence of the listener, is that right? Literally, I cut the connection.

 

Well, you probably just took the next best term and I will surely find something more more fitting.

Nonetheless, it really seems to suggest more, are there any other use cases (probably off-topic though)?

Share this post


Link to post
Share on other sites

I thought a bit more about it and "lifetime" seems to be an odd name for it. Doesn't it semantically indicate the lifetime of the listener?


Hence why I named it that. :)

If I'm right, being unable to listen (listener has been destroyed) would need no lifetime-object, as the caller could just check the pointer, right?


You don't _need_ the Lifetime object, but you need to hold onto something to get RAII; e.g., if a Lifetime is a member variable of a class and the class is destructed, you want anything it registered on its own lifetime to become invalidated. If you use shared_ptr/weak_ptr implementation (not ideal, but it works) then you _could_ just use the pointers themselves, but that loses semantics; it's not at all obvious or clear by looking at the code what the shared_ptr is actually for. In my actual code, I split the type into a Lifetime (tracks validity of the resource; aka the shared_ptr in the example) and LifetimeObserver (observes the lifetime's validity; aka the weak_ptr in the example).

The reset-method feels like I make the caller forget about the existence of the listener, is that right? Literally, I cut the connection.


Yup. It's identical to the .reset() method on the shared_ptr in your original example, except it controls anything registered against the Lifetime instead of just one thing.

If you go with something like your original implementation mixed with mine, a Lifetime might just hold onto a vector<shared_ptr<Listener>> which is .clear()'d whenever you want to reset all the registered listeners it owns.

Another implementation I know of used in a lot of shipping code is to have the "Lifetime" hold onto a linked-list of nodes, which the listeners themselves being a linked-list of nodes, and each node being in both linked lists. Then when the "lifetime" is cleared it can find and delete all the listeners, and when the emitter is clear it also can find and delete all the listeners.

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