Jump to content
  • Advertisement
Sign in to follow this  
Shadeirou

[C++] Follow-up on my event system

This topic is 2534 days old which is more than the 365 day threshold we allow for new replies. Please post a new topic.

If you intended to correct an error in the post then please contact us.

Recommended Posts

This post is a follow-up to another of my posts, located here, where I asked for feedback on my event system. I have since had time to revisit my EventDispatcher class and have added methods for unregistration of listeners via unique ID or object. The system, as is, works well enough and is efficient, but unregistration of member functions is not guaranteed when an object is destroyed. That is the part I have been hung up on. Here is the code for the current version:

SpecificEventDispatcher.h:

namespace inp {
// Description: SpecificEventDispatcherBase is the base class for all
// SpecificEventDispatcher classes.
class SpecificEventDispatcherBase {
public:
virtual ~SpecificEventDispatcherBase() {}
virtual void UnregisterByID(uint32_t id) = 0;
virtual void UnregisterByObject(void* obj) = 0;
};

// Description: SpecificEventDispatcher is used to dispatch a specific type of
// event.
template<typename EventT>
class SpecificEventDispatcher : public SpecificEventDispatcherBase {
public:
// public functions --------------------------------------------------------
// register listeners: Registers a function or member function and returns
// a unique ID that can be used to unregister later.
uint32_t Register(int (*func)(const EventT&));

template<typename ClassT>
uint32_t Register(int (ClassT::*func)(const EventT&), ClassT* obj);

// unregister listeners
void UnregisterByID(uint32_t id);
void UnregisterByObject(void* obj);

// dispatch event
void Dispatch(const EventT& event);

private:
// types -------------------------------------------------------------------
struct Callback {
// typedefs
typedef std::function<int(const EventT&)> Function;

// ctor
Callback(Function func, void* obj) : function(func), object(obj) {
static uint32_t static_id = 0;
id = static_id;
++static_id;
}

// vars
Function function;
void* object; // I really don't like this reliance on void pointers...
uint32_t id;
};

// vars --------------------------------------------------------------------
std::vector<Callback> listeners_;
};

// public functions ------------------------------------------------------------
// register listeners
template<typename EventT>
uint32_t SpecificEventDispatcher<EventT>::Register(int (*func)(const EventT&)) {
Callback callback(func, NULL);
listeners_.push_back(callback);
return callback.id;
}

template<typename EventT> template<typename ClassT>
uint32_t SpecificEventDispatcher<EventT>::Register(int (ClassT::*func)(const EventT&), ClassT* obj) {
Callback callback(std::bind(func, obj, std::placeholders::_1), obj);
listeners_.push_back(callback);
return callback.id;
}

// unregister listeners
template<typename EventT>
void SpecificEventDispatcher<EventT>::UnregisterByID(uint32_t id) {
for (auto itr = listeners_.begin(); itr != listeners_.end(); ++itr) {
if (itr->id == id) {
listeners_.erase(itr);
return;
}
}
}

template<typename EventT>
void SpecificEventDispatcher<EventT>::UnregisterByObject(void* obj) {
for (auto itr = listeners_.begin(); itr != listeners_.end(); ++itr) {
if (itr->object == obj) {
listeners_.erase(itr);
return;
}
}
}

// dispatch event
template<typename EventT>
void SpecificEventDispatcher<EventT>::Dispatch(const EventT& event) {
for (auto itr = listeners_.begin(); itr != listeners_.end(); ++itr) {
itr->function(event);
}
}

} // namespace inp


EventDispatcher.h:

namespace inp {

// Description: EventDispatcher is used to dispatch a variety of event types to
// a variety of listeners.
class EventDispatcher {
public:
// ctors -------------------------------------------------------------------
EventDispatcher();

// public functions --------------------------------------------------------
// register listeners: Registers a function or member function and returns
// a unique ID that can be used to unregister later.
template<typename EventT>
uint32_t Register(int type, int (*func)(const EventT&));

template<typename ClassT, typename EventT>
uint32_t Register(int type, int (ClassT::*func)(const EventT&), ClassT* obj);

// unregister
void UnregisterByID(int type, uint32_t id);
void UnregisterByObject(int type, void* obj);

// dispatch events
template<typename EventT>
void Dispatch(int type, const EventT& event);

private:
// vars --------------------------------------------------------------------
std::map<int, SpecificEventDispatcherBase*> listeners_;
};

// public functions ------------------------------------------------------------
// register listeners
template<typename EventT>
uint32_t EventDispatcher::Register(int type, int (*func)(const EventT&)) {
// grab the iterator for the event id
auto itr = listeners_.find(type);
SpecificEventDispatcher<EventT>* sed = NULL;
if (itr == listeners_.end()) { // iterator not found
sed = new SpecificEventDispatcher<EventT>();
listeners_.insert(std::make_pair(type, sed));
} else { // iterator found
sed = static_cast<SpecificEventDispatcher<EventT>*>(itr->second);
}
// register the function
return sed->Register(func);
}

template<typename ClassT, typename EventT>
uint32_t EventDispatcher::Register(int type, int (ClassT::*func)(const EventT&), ClassT* obj) {
if (obj == NULL) { return NULL; } // disallow NULL objects
// grab the iterator for the event id
auto itr = listeners_.find(type);
SpecificEventDispatcher<EventT>* sed = NULL;
if (itr == listeners_.end()) { // iterator not found
sed = new SpecificEventDispatcher<EventT>();
listeners_.insert(std::make_pair(type, sed));
} else { // iterator found
sed = static_cast<SpecificEventDispatcher<EventT>*>(itr->second);
}
// register the function
return sed->Register(func, obj);
}

// dispatch events
template<typename EventT>
void EventDispatcher::Dispatch(int type, const EventT& event) {
// grab the iterator for the event id
auto itr = listeners_.find(type);
if (itr != listeners_.end()) { // iterator found
static_cast<SpecificEventDispatcher<EventT>*>(itr->second)->Dispatch(event);
}
}

} // namespace inp


EventDispatcher.cpp:

namespace inp {

// ctors -----------------------------------------------------------------------
EventDispatcher::EventDispatcher() {
// initializes the map to prevent "breaks strict-aliasing" warning
listeners_[-1] = NULL;
}

// public functions ------------------------------------------------------------
// unregister
void EventDispatcher::UnregisterByID(int type, uint32_t id) {
// grab the iterator for the event id
auto itr = listeners_.find(type);
// if the iterator was found, unregister
if (itr != listeners_.end()) { itr->second->UnregisterByID(id); }
}

void EventDispatcher::UnregisterByObject(int type, void* obj) {
if (obj == NULL) { return; } // disallow NULL objects
// grab the iterator for the event id
auto itr = listeners_.find(type);
// if the iterator was found, unregister
if (itr != listeners_.end()) { itr->second->UnregisterByObject(obj); }
}

} // namespace inp


Now, what I would like for this system is for registered objects to unregister themselves when their destructor is called, but in order to do this I would need to store a pointer to each dispatcher it was registered to, then loop through and unregister the object. And while that is simple enough, it creates a dependency to the dispatchers that I would prefer not to have. And if a dispatcher an object is registered to is deleted, destroying that object would cause the program to crash. So, my solution was to create an interface, IEventListener, that had 2 methods: OnRegisteredToDispatcher and OnUnregisteredFromDispatcher. With this, the dispatcher could call OnUnregisteredFromDispatcher when it was deleted, informing the object it was no longer registered. This also allowed me to remove the reliance on void pointers for storing the object for comparison (which I suppose could have also been avoided by removing the ability to unregister by object). But here, the issue becomes the actual implementation of IEventListener methods by the objects inheriting from it (and it implies a relationship between classes where one does not necessarily exist, but I'm not too worried about that). What I mean by this is that all classes inheriting from IEventListener will basically have the same code regarding the two methods:


class Listener : public IEventListener {
public:
virtual ~Listener() {
for (size_t x = 0; x < dispatchers_.size(); ++x) {
dispatchers_[x]->UnregisterByObject(this);
}
}

void OnRegisteredToDispatcher(EventDispatcher* d) {
dispatchers_.push_back(d);
}

void OnUnregisteredFromDispatcher(EventDispatcher* d) {
for (size_t x = 0; x < dispatchers_.size(); ++x) {
if (dispatchers_[x] == d) {
dispatchers_.erase(dispatchers_.begin() + x);
}
}
}

private:
std::vector<EventDispatcher*> dispatchers_;
}


With that in mind, I would want to have all my listeners inherit from that class instead, since all listeners should implement the interface's methods like that anyway. But then the problem with that is any class that already inherits from another class would have to rely on multiple implementation inheritance, which I am not too fond of.

So, should I just have all the listeners inherit from a class similar to Listener above and not worry about multiple implementation inheritance? Or is there another way to accomplish the same thing that I'm not thinking of? Or should I use smart pointers in the Callback class to check for the deleted objects (not ideal afaik; adds the overhead of checking the object associated with the callback each pass of the dispatch method's loop)?

Requirements for the system:

  1. Objects that register member functions should unregister themselves upon destruction
  2. If objects are required to be aware of dispatchers they are registered to, the object should know when one of those dispatchers has been destroyed
  3. Performance

Desired attributes for the system:

  1. Lessened dependence on void pointers
  2. Avoidance of multiple implementation inheritance
  3. Loosely coupled objects


Thanks in advance.

Share this post


Link to post
Share on other sites
Advertisement
As I mentioned in the other thread one way around this problem is that instead of using objects and ids you store iterators to the objects in the registering object and instead of using an std::vector to store callback objects use a (pool allocated) list instead as list iterators remain valid after deletes/adds from a list unlike vector ones which are invalidated in such cases.

When it comes to removing things RAII can do a lot of the heavy lifting if you wrap things in an object which handles unregistering events of a certain 'type' for you. At which point is can have a pointer back to the correct specific dispatcher internally to unregister meaning your registering class doesn't need to know about it.

Now, as to 'what happens IF the dispatcher goes out of scope before the listening object' well honestly is that likely to happen and NOT be a bug? While a dispatcher has someone registered against it then it shouldn't be able to shut down as it seems like it would invalidate the integrity of the program. So, my first thought would be not to worry about it and just asset if you try to kill a dispatcher with things registered to it still.

However if you really want to guard against this then one solution which springs to mind is to reserve some memory to hold a pointer to the specific dispatcher in question then give all the RAII clean up objects a pointer to it (shared_ptr or something like it as you'll need to clean it up when everyone has done with it). When your dispatcher shuts down it sets this memory location to 'null' before dying. Then all unregistering objects must check for 'null' before trying to unregister; when the last one goes out of scope the shared_ptr will clean up the memory.

This does come with the overhead of, slightly, slowing the unregistering down as everything will have to make that check and in 99.999999999999% of cases I suspect it will be a case of 'yes, my dispatcher exists" as well as a cache miss or two. However as unregistering will be a rare event, in general terms, this might not be a problem.

Personally I would stick with 'no dispatcher can be shut down without unregistering things from it first' to just avoid the problem completely.

(I would provide code examples but I'm on a laptop atm and about to vanish for NYE drinks; if you want more details/code then wait until the middle/late next week when I'll be a) sober again and b) back on my desktop)

Share this post


Link to post
Share on other sites
Hey phantom, thanks for revisiting this with me smile.png.


As I mentioned in the other thread one way around this problem is that instead of using objects and ids you store iterators to the objects in the registering object and instead of using an std::vector to store callback objects use a (pool allocated) list instead as list iterators remain valid after deletes/adds from a list unlike vector ones which are invalidated in such cases.

Yes, actually I played around with implementing something like that after you suggested it and after reading through the debate between you and speciesUnknown here, but it seemed to slow the dispatching processes on my desktop by ~22% (from an average of ~905 ms to ~ 1100 ms, tested with 5 different event types, 200 dispatches of each, and 10000 listeners), but didn't slow the process at all on my laptop. The only thing I can figure there (speculation incoming) is that my desktop has an AMD processor while my laptop has an Intel and the way it compiles/runs for each chipset is different?


Now, as to 'what happens IF the dispatcher goes out of scope before the listening object' well honestly is that likely to happen and NOT be a bug?


The situation I've been trying to keep in mind goes something like this:

  1. An instance of SomeListener wants to be notified when an instance of SomeWidget is clicked on -> SomeListener registers to a dispatcher internal to SomeWidget.
  2. The instance of SomeWidget is no longer needed, so it is destroyed -> internal dispatcher is destroyed, unregistering all listeners as far as the dispatcher is concerned (objects aren't informed because they didn't provide any way to be informed).
  3. The instance of SomeListener is no longer needed, so it is destroyed -> attempts to unregister itself from a dispatcher that no longer exists.

My initial solution was going to be using a WidgetManager type and just registering to it, but then, instead of just getting messages about the single widget, the listener would get messages when any widget in the manager was clicked and would then have to determine if that widget was the right one. This would certainly be more costly.


While a dispatcher has someone registered against it then it shouldn't be able to shut down as it seems like it would invalidate the integrity of the program. So, my first thought would be not to worry about it and just asset if you try to kill a dispatcher with things registered to it still.

I guess I never really thought of it that way. I suppose I could just keep dispatchers alive until everything is unregistered from them, but if, for whatever reason, I needed to get rid of a dispatcher I would have to remove each listener before destroying it. That would mean either looking through the program, manually removing each function/object that is registered, and informing it that it has been unregistered from the outside (which seems unreliable), or keeping a list of all functions/objects registered and unregister and inform them by iterating through it (which is basically where I am now).


However if you really want to guard against this then one solution which springs to mind is to reserve some memory to hold a pointer to the specific dispatcher in question then give all the RAII clean up objects a pointer to it (shared_ptr or something like it as you'll need to clean it up when everyone has done with it). When your dispatcher shuts down it sets this memory location to 'null' before dying. Then all unregistering objects must check for 'null' before trying to unregister; when the last one goes out of scope the shared_ptr will clean up the memory.

This does come with the overhead of, slightly, slowing the unregistering down as everything will have to make that check and in 99.999999999999% of cases I suspect it will be a case of 'yes, my dispatcher exists" as well as a cache miss or two. However as unregistering will be a rare event, in general terms, this might not be a problem.

Interesting. I've had very, very, very little experience with smart pointers, so this might be something to explore. And I agree, unregistering should be a rare event. The only overhead I care much about is that caused by dispatching events.


Personally I would stick with 'no dispatcher can be shut down without unregistering things from it first' to just avoid the problem completely.


Honestly, you are probably right. The more I think about it the less and less I can see this being a problem. My brain isn't letting me let it go for some reason though unsure.png.

Changing subjects a bit here, registered objects still need to unregister themselves when their destructor gets called, which means each will have to implement a method of storing when registered and unregistering when being destroyed which should, in theory, be the same implementation for every object. Knowing that, it seems like the best thing to do would be to implement that once in a Listener class and have listeners derive from it. This is where my issue with multiple implementation inheritance comes into play. I can think of a couple of ways to do it using composition instead, but they all seem to make things more complicated.


(I would provide code examples but I'm on a laptop atm and about to vanish for NYE drinks; if you want more details/code then wait until the middle/late next week when I'll be a) sober again and b) back on my desktop)

happy.png

Share this post


Link to post
Share on other sites

If callbacks are added or removed from within callback, funny stuff will happen.

This is true, which is why the Callback structure is private, internal to SpecificEventDispatcher, and no pointers or references to an instance of Callback are returned. Callback is only there to pair a unique ID and object with the function object.

Share this post


Link to post
Share on other sites

[quote name='Antheus' timestamp='1325437469' post='4898691']
If callbacks are added or removed from within callback, funny stuff will happen.

This is true, which is why the Callback structure is private, internal to SpecificEventDispatcher, and no pointers or references to an instance of Callback are returned. Callback is only there to pair a unique ID and object with the function object.
[/quote]

I meant this.

Share this post


Link to post
Share on other sites
Sign in to follow this  

  • Advertisement
×

Important Information

By using GameDev.net, you agree to our community Guidelines, Terms of Use, and Privacy Policy.

GameDev.net is your game development community. Create an account for your GameDev Portfolio and participate in the largest developer community in the games industry.

Sign me up!