Event System, is this a decent design?

Started by
36 comments, last by schragnasher 14 years, 10 months ago
Quote:Original post by Sneftel
Quote:Original post by aaron_ds
Straw man. Events can easily be placed in a hash container with O(1) lookup.
Asymptotic time complexity is not the only measure of efficiency. Show me a hash table with the same speed and memory usage as direct variable access. :)

Touche! :)

Quote:Original post by Sneftel
Quote:Original post by aaron_ds
I'm using MVC + an event manager to proxy the observer pattern between Views and Models and Views and Controllers.

There's nothing wrong with this approach, and it can't be beaten for ease of extension, but yeah, I'd argue that injecting a structure of listeners instead would give you greater code clarity. Whenever I find myself reaching for boost::any, I think to myself "If what I want is dynamic typing, why am I using a statically typed language?" [grin]


Exactly! It worked as a proof of concept, but after reading your thoughts I'm excited to work it out with plain old variables and static typing. I still maintain that it tightly couples the event manager to the application domain, but the benefits gained from the compile-time checks could offset it.

Advertisement
I think it looks good. I would make the classes that listen to the events register their event handlers with the event manager. This helps you register sub-objects events. ie if a scroll bar is made up of a slider and two buttons, the slider and button's event handlers need to be registered when the scroll bar's register event handlers function is called.

void cButton::registerEventHandles ( cEventHandler * EventHandler ) {	EventHandler->registerEventHandle ( this, &cButton::handleMouseButton );	EventHandler->registerEventHandle ( this, &cButton::handleMouseMove );};bool cButton::handleMouseButton ( const cMouseButtonEvent *ButtonEvent ){// handle mose button event}bool cButton::handleMouseMove ( const cMouseMoveEvent *MoveEvent ){// handle mouse move event}


I would also use static strings for the event names and event paramaters would be members in inherited classes from cEvent. Hard coding events shouldnt be a problem. You dont have a PlayerMoveRight event, you have a PlayerMove event, and the paramaters tell the system which way the player moved.
#define GIVE_NAME( NAME )			virtual const cString getName ( ) const		{							static cString name ( #NAME );			return name;				}// never used directly, use inherited classes below insteadclass cMouseEvent : public cEvent {public:	cMouseEvent ( const int x, const int y ) : mX ( x ), mY ( y ), cEvent ( &cMouseTarget::mTaget, 0.0f ) { }	int mX;	int mY;};class cMouseButtonEvent : public cMouseEvent{public:	cMouseButtonEvent ( 		const int x, 		const int y, 		const int button, 		const bool pressed		) : 		cMouseEvent ( x, y ),		mButton ( button ),		mPressed ( pressed)		{		}	GIVE_NAME ( cMouseButtonEvent );	int mButton;	bool mPressed;};class cMouseMoveEvent : public cMouseEvent{public:	cMouseMoveEvent ( 		const int x, 		const int y,		const int dx, 		const int dy		) : 		cMouseEvent ( x, y ), mDX ( dx ), mDY ( dy )		{		}	GIVE_NAME ( cMouseMoveEvent );	int mDX;	int mDY;};class cMouseScrollEvent : public cMouseEvent{public:	cMouseScrollEvent ( 		const int x, 		const int y, 		const int scrollValue		) : 		cMouseEvent ( x, y ),		mScrollValue ( scrollValue )		{		}	GIVE_NAME ( cMouseScrollEvent );	int mScrollValue;};


Hope that makes sence.

EDIT:
I forgot to mention that if your strings are stored in a hash table, then string compares reduce to a pointer compare.

The registerEventHandle method can also figure out the type of event that the handler is for based on the prototype of the handler. it knows that

bool cButton::handleMouseMove ( const cMouseMoveEvent *MoveEvent )

is for cMouseMoveEvent's because thats the paramater it takes. You can do this with template paramater deduction as follows:
	template < class T, class cEventT >	void registerEventHandle ( T* obj, bool ( T::*memFn )( cEventT* ) )	{		cString eventName ( cTypeInfo::getTypeInfo < cEventT > ( ).mTypeName );		cArray < cEventHandle * > ** array = mHandles.findItem ( eventName );		if ( !array || !*array )		{            array = mHandles.insertItem ( eventName, new cArray < cEventHandle * > ( ) );		}		ASSERT ( array );		(*array)->add ( new cMemberEventHandle<T, cEventT>(obj, memFn) );	}
Quote:Original post by thefries
I think it looks good. I would make the classes that listen to the events register their event handlers with the event manager. This helps you register sub-objects events. ie if a scroll bar is made up of a slider and two buttons, the slider and button's event handlers need to be registered when the scroll bar's register event handlers function is called.

*** Source Snippet Removed ***

I would also use static strings for the event names and event paramaters would be members in inherited classes from cEvent. Hard coding events shouldnt be a problem. You dont have a PlayerMoveRight event, you have a PlayerMove event, and the paramaters tell the system which way the player moved.
*** Source Snippet Removed ***

Hope that makes sence.

EDIT:
I forgot to mention that if your strings are stored in a hash table, then string compares reduce to a pointer compare.

The registerEventHandle method can also figure out the type of event that the handler is for based on the prototype of the handler. it knows that

bool cButton::handleMouseMove ( const cMouseMoveEvent *MoveEvent )

is for cMouseMoveEvent's because thats the paramater it takes. You can do this with template paramater deduction as follows:
*** Source Snippet Removed ***


Thanks, the last bit there is very useful to know, very interesting.
if you change the #define to something like this

#define GIVE_NAME( NAME )
static const cString m_name = cString ( #NAME );
virtual const cString getName ( ) const
{
return m_name;
}

then this line:
cString eventName ( cTypeInfo::getTypeInfo < cEventT > ( ).mTypeName );

would become something like this:

cString eventName = cEventT::m_name;

And that removes some of the dependencies on my system internals with out having to use RTTI.

Quote:Original post by Sneftel
I'm really not a fan of a centralized "event manager" which sticks all the events into one sprawling, inefficient container and then searches through for the one or two listeners out of thousands which actually give a damn about a given message. C++ gives you as many variables as you want. USE THEM! If you need an event that can be fired and listened to, make an event. You can put all the events in a special namespace if that makes things feel tidier to you. You'll get faster access, compile-time checking of event parameter types, better debuggability, and better separation of concerns.

Can you clarify this design a little bit? Are you saying to just have the event listeners just be individual objects without a manager? Do you have any sample code to demonstrate what you mean? It's an intriguing idea, and I would like to hear more about it.

Quote:Original post by Jason Z
Can you clarify this design a little bit? Are you saying to just have the event listeners just be individual objects without a manager?
Events are just individual objects without a manager. You can think of them as tiny managers which each manage only one event, if you like. Some example code off the top of my head:
class Gun{   ...public:   event<Gun*, bool /*silenced*/> GunFired;   ...   void FireGun()   {       GunFired.Invoke(this, m_silenced);       // Other stuff involved in firing the gun   }};class SoundEngine{   ...   void GunFiredListener(Gun* gun, bool silenced)   {      this->PlaySound(gun->GetPosition(), silenced ? SOUND_SILENCED_GUNFIRE : SOUND_GUNFIRE);   }   ...};void AddGunToWorld(Gun* gun){    gun->GunFired.Attach(g_soundEngine, SoundEngine::GunFiredListener);}


Obviously, the syntax there depends heavily on which event/signal/delegates/whatever library you're using.
Quote:Original post by Sneftel
*** Source Snippet Removed ***
Obviously, the syntax there depends heavily on which event/signal/delegates/whatever library you're using.


That's almost exactly what I'm doing myself, and it's neat and fast. What I'm doing is kinda like the reverse of this. The GunFired event would be static (which gun is being fired is already known to the listeners through the first parameter), and the listener object can only listen to one event. Adding and removing listeners to events in any order is done in constant time with no need for dynamic memory allocation (the event object derives from a doubly-linked list class, and the listener objects derive from the node class of that list class).

The only issue I have is that the listener object requires a function pointer (whose type is determined by the event type), so I often find myself writing little wrapper functions that call methods on objects for use with the listener object. I considered Member Function Pointers and the Fastest Possible C++ Delegates by Don Clugston to remove the issue, but it uses "a horrible hack" which is not compatible with the C++ standard. I wonder if there is another way around that issue.
Quote:Original post by hikikomori-san
The only issue I have is that the listener object requires a function pointer (whose type is determined by the event type), so I often find myself writing little wrapper functions that call methods on objects for use with the listener object. I considered Member Function Pointers and the Fastest Possible C++ Delegates by Don Clugston to remove the issue, but it uses "a horrible hack" which is not compatible with the C++ standard. I wonder if there is another way around that issue.
There is. It's clever, efficient, and completely standards-compliant. And it doesn't work on a lot of compilers. Ironic, no? Standards compliance, while desirable, is neither necessary nor sufficient for robust code. Don Clugston's code is in wide use on all major platforms... I think that speaks much louder than standards compliance.
Here is a listing of an event system I've become rather fond of.

EventSender.h
#include "EventListener.h"template<typename EventType> class EventListener;template<typename EventType>class EventSender{public:	typedef EventListener<EventType> ListenerType;private:	typedef std::vector<ListenerType*> EventListenerVector;	EventListenerVector mListeners;	static EventSender<EventType>& GetInstance()	{		static EventSender<EventType> sender;		return sender;	}	void AddListener( ListenerType* theListener )	{		mListeners.push_back( theListener );	}	void RemoveListener( const ListenerType* theListener )	{		EventListenerVector::iterator l = std::remove(			mListeners.begin(), mListeners.end(), theListener );		if ( l != mListeners.end() )			mListeners.erase( l );	}	void SendEvent( const EventType& theEvent )	{		EventListenerVector::iterator l;		for ( l = mListeners.begin(); l != mListeners.end(); ++l )			(*l)->HandleEvent( theEvent );	}public:	virtual ~EventSender() {}	static void Add( ListenerType* theListener )	{		GetInstance().AddListener( theListener );	}	static void Remove( const ListenerType* theListener )	{		GetInstance().RemoveListener( theListener );	}	static void Send( const EventType& theEvent )	{		GetInstance().SendEvent( theEvent );	}};


EventListener.h
#include "EventSender.h"template<class EventType>class EventListener{public:	EventListener()	{		EventSender<EventType>::Add( this );	}	virtual ~EventListener()	{		EventSender<EventType>::Remove( this );	}	virtual void HandleEvent( const EventType& theEvent ) = 0;};


Usage
class ResetScoreEvent {};class LevelChangedEvent{public:	LevelChangedEvent( const std::string& filename ) : mFilename( filename ) {}	std::string	mFilename;};class Scoreboard : public EventListener<LevelChangedEvent>,	public EventListener<ResetScoreEvent>{	void HandleEvent( const LevelChangedEvent& event )	{		SetLevelName( event.mFilename );	}		void HandleEvent( const ResetScoreEvent& event )	{		SetScore( 0 );	}		//...}void ChangeLevel( const std::string& filename ){	//...	EventSender<ResetScoreEvent>::Send( ResetScoreEvent() );	EventSender<LevelChangedEvent>::Send( LevelChangedEvent( filename ) );}


Pros:
Events are passed to listeners by reference with no need for dynamic_cast.
Event listeners use RAII to handle their own registration.
Doesn't require a base event class, or any centralized location for event definitions.
Arguments are passed with the event itself.

Cons:
Singleton design of EventSender.
Listeners must inherit from EventListener for each event.
Quote:Original post by Sneftel
Quote:Original post by hikikomori-san
The only issue I have is that the listener object requires a function pointer (whose type is determined by the event type), so I often find myself writing little wrapper functions that call methods on objects for use with the listener object. I considered Member Function Pointers and the Fastest Possible C++ Delegates by Don Clugston to remove the issue, but it uses "a horrible hack" which is not compatible with the C++ standard. I wonder if there is another way around that issue.
There is. It's clever, efficient, and completely standards-compliant. And it doesn't work on a lot of compilers. Ironic, no? Standards compliance, while desirable, is neither necessary nor sufficient for robust code. Don Clugston's code is in wide use on all major platforms... I think that speaks much louder than standards compliance.

Wow - those are great articles! Admittedly I don't completely understand all of the discussion of member function pointers (I didn't even know they existed until I read that article), but the spirit of delegates is clear and the motivation for a fast implementation of them is clear as well. Thank you for the links - I need to digest some of this information...

This topic is closed to new replies.

Advertisement