C++ Event/Delegate System

Started by
6 comments, last by robinei 14 years, 8 months ago
Here is a simple event/delegate system I've been working on as part of a framework I'm doing. I make use of a Fast Delegate which can be found at here (requires some boost headers).

/// Traits structure specialized for defined events using the event definition macros.
template< typename T >
struct ArgumentTraits
{
};

template< typename T >
class Event : Noncopyable // Cannot copy events.
{
public:

	/// Arguments typedef for convenience.
	typedef ArgumentTraits< T > Arguments;
	/// Listener function pointer type.
	typedef void ( * ListenerFunc )( Arguments const & );
	
	/// Get global event.
	static Event< T > & Global();

	/// Empty constructor.
	Event() {}

	/// Empty destructor.
	~Event() {}

	/// Raise an event with arguments.
	void Raise( Arguments const & args );

	/// Register a listener function to the event.
	void Register( ListenerFunc func );

	/// Increment operator used to register a listener function to the event.
	void operator += ( ListenerFunc func );

	/// Unregister a listener function from the event.
	void Unregister( ListenerFunc func );

	/// Decrement operator used to unregister a listener function from the event.
	void operator -= ( ListenerFunc func );

	/// Register a listener object to the event.
	template< typename O >
	void Register( O & obj );
	
	/// Increment operator used to register a listener object to the event.
	template< typename O >
	void operator += ( O & obj );

	/// Unregister a listener object from the event.
	template< typename O >
	void Unregister( O & obj );

	/// Decrement operator used to unregister a listener object from the event.
	template< typename O >
	void operator -= ( O & obj );

	/// Clears all registered event listeners.
	void Clear();

private:

	fd::delegate1< void, Arguments const & > mDelegate; ///< Event delegate.
};

template< typename T >
Event< T > & Event< T >::Global()
{
	static Event< T > global;
	return global;
}

template< typename T >
void Event< T >::Raise( Arguments const & args )
{
	if( mDelegate.count() )
		mDelegate( args );
}

template< typename T >
void Event< T >::operator += ( ListenerFunc func )
{
	Register( func );
}

template< typename T >
void Event< T >::operator -= ( ListenerFunc func )
{
	Unregister( func );
}

template< typename T >
void Event< T >::Register( ListenerFunc func )
{
	mDelegate.add( func );
}

template< typename T >
void Event< T >::Unregister( ListenerFunc func )
{
	mDelegate.remove( func );
}

template< typename T >
template< typename O >
void Event< T >::Register( O & obj )
{
	mDelegate.add( boost::ref( obj ) );
}

template< typename T >
template< typename O >
void Event< T >::operator += ( O & obj )
{
	Register( obj );
}

template< typename T >
template< typename O >
void Event< T >::Unregister( O & obj )
{
	mDelegate.remove( boost::ref( obj ) );
}

template< typename T >
template< typename O >
void Event< T >::operator -= ( O & obj )
{
	Unregister( obj );
}

/// Macro to define a new event.
/// Arguments is typedef-ed for convenience, as seen below.
#define BEGIN_EVENT_DEF( eventName ) 	struct eventName { typedef ::ArgumentTraits< eventName > Arguments; }; 	template<> struct ::ArgumentTraits< eventName > {

/// Close event definition.
#define END_EVENT_DEF };

For objects to register with the event, it must implement the () operator. Usage:

BEGIN_EVENT_DEF( OnTest )
	int x;
	int y;
END_EVENT_DEF

class TestListener
{
public:
	void operator () ( OnTest::Arguments const & args )
	{
		printf( "[Object] %d, %d", args.x, args.y );
	}
};

void TestFunction( OnTest::Arguments const & args )
{
	printf( "[Function] %d, %d", args.x, args.y );
}

Event< OnTest > evt;
TestListener listener;

evt.Register( listener );
evt.Register( TestFunction );
evt.Raise( data );
evt.Unregister( listener );
evt.Unregister( TestFunction );
evt.Raise( data );

// OR

evt += listener;
evt += TestFunction ;
evt.Raise( data );
evt -= listener;
evt -= TestFunction;
evt.Raise( data );

The Global() function is not needed but is useful for global events that need to be triggered. Feedback is appreciated. :)
Advertisement
Looks pretty nice and clean, cleaner then my solution. However, it only executes operator() of a class, right? Might be nice to be able to register any member function of a class...

Also, do you really need FastDelegate?
[size="2"]SignatureShuffle: [size="2"]Random signature images on fora
I went on the basis that I hardly ever use the () operator for anything else and that I probably wouldn't want to ever register the same object to an event more than once. I can simply do this:

class Listener{public:	void operator () ( OnTest::Arguments const & args )	{		DoSomething( args );		DoSomethingElse( args );	}		void DoSomething( OnTest::Arguments const & args );	void DoSomethingElse( OnTest::Arguments const & args );};


Or if I need to register under 2 different events, I would just overload another () operator with different event arguments.

It helps to keep a cleaner interface and allows the usage of += and -= operators without having to use any complicated function object binding.

I tried using
std::list< boost::function >
or
std::list< std::tr1::function >
but I wasn't able to get it working with unregistering object functions (== operator doesn't automagically compare between two std::tr1::function objects).
I considered using other delegate systems such as boost::signals and libsigc++ but I wanted a header-only implementation without having to include external .lib files to reduce the dependencies required by the framework.

Please do share if you have any suggestions on how I could implement the delegate system more efficiently! Thanks!

EDIT: fd::delegate is also pretty fast from the benchmarks in the article I linked above. Boost has a new signals library called boost::signal2 and it's header-only implementation so it might be worth checking out :)

[Edited by - auron777 on July 28, 2009 8:38:35 AM]
I really cannot talk about efficiency here, I have not tested it speed-wise or even pondered about.

I always find it fun to figure it out myself, but this is basically what I use:

    class Slot    {    public:        virtual Slot *clone() const = 0;        virtual void operator()(void *emitter) = 0;    };    class FunctionSlot: public Slot    {        typedef void (*FUNCTION)(); // no parameters, but you could use your Argument class for this    public:        FunctionSlot(FUNCTION ifunction);        FunctionSlot(const FunctionSlot &instance);        FunctionSlot *clone() const;        FUNCTION function() const;        void operator()(void *emitter);    private:        FUNCTION _function;    };    template <class T> // T = class    class MemberFunctionSlot: public Slot    {        typedef T *OBJECT;        typedef void (T::*MEMBER_FUNCTION)(); // no parameters, but you could use your Argument class for this    public:        MemberFunctionSlot(OBJECT iobject, MEMBER_FUNCTION imember_function);        MemberFunctionSlot(const MemberFunctionSlot<T> &instance);        MemberFunctionSlot<T> *clone() const;        OBJECT object() const;        MEMBER_FUNCTION member_function() const;        void object(OBJECT iobject);        void operator()(void *emitter);    private:        OBJECT _object;        MEMBER_FUNCTION _member_function;    };


In a Signal class I then have an array of Slot pointers. I have fixed arguments, so that made it much easier. I'm unsure if void pointers are not giving undefined behavior here for some obscure reason, but it seems to work fine so far ;).
[size="2"]SignatureShuffle: [size="2"]Random signature images on fora
I'm trying to avoid the usage of virtual classes since you probably don't ever need more than 2 types of slots.
fd::delegate avoids virtual classes as far as I can tell.
I'll try to implement a simple delegate system as well when I have time :)
Here's code for an event system I made in C. Since it is programmed using macros it does not have to throw type safety away.
Events are defined by for example "evt_define(foobar2, int, float, int)". This can be used anywhere where declaring a variable is legal, such as within a struct. evt_typedef can also be used to capture the otherwise anonymous type.
The first type argument is the data that will be bound together with the handler function, and the rest are the types of the arguments that are provided when signaling the event.

The ##__VA_ARGS__ construct which will eat the leading comma when no arguments are provided is non-standard, but works in MSVC and GCC at least.

#include <stdio.h>#include <stdlib.h>// replace //\ with \#define evt_define(Name, DataType, ...)                                      //\    struct {                                                                 //\        short num, max;                                                      //\        union {                                                              //\            struct {                                                         //\                void (*func)(DataType, ##__VA_ARGS__);                       //\                DataType data;                                               //\            } *binds;                                                        //\            void *untyped;                                                   //\        };                                                                   //\    } Name#define evt_typedef(Type, DataType, ...)                                     //\    typedef evt_define(Type, DataType, ##__VA_ARGS__)#define evt_signal(Evt, ...)                                                 //\    do {                                                                     //\        int i;                                                               //\        for(i = 0; i < (Evt).num; ++i)                                       //\            (Evt).binds.func((Evt).binds.data, ##__VA_ARGS__);         //\    } while(0)#define evt_bind(Evt, Func, Data)                                            //\    do {                                                                     //\        int num = (Evt).num;                                                 //\        int max = (Evt).max;                                                 //\        if(num == max) {                                                     //\            if(max) max *= 2;                                                //\            else max = 2;                                                    //\            (Evt).untyped = realloc((Evt).binds, sizeof(*(Evt).binds) * max);//\            (Evt).max = max;                                                 //\        }                                                                    //\        (Evt).binds[num].func = Func;                                        //\        (Evt).binds[num].data = Data;                                        //\        ++(Evt).num;                                                         //\    } while(0)#define evt_unbind(Evt, Func, Data)                                          //\    do {                                                                     //\        int i;                                                               //\        for(i = 0; i < (Evt).num; ++i) {                                     //\            if((Func) == (Evt).binds.func &&                              //\               (Data) == (Evt).binds.data) {                              //\                for((Evt).num -= 1; i < (Evt).num; ++i) {                    //\                    (Evt).binds.func = (Evt).binds[i+1].func;             //\                    (Evt).binds.data = (Evt).binds[i+1].data;             //\                }                                                            //\                break;                                                       //\            }                                                                //\        }                                                                    //\    } while(0)static void on_foobar(int data, float f, int i) {    printf("on_foobar %d\nargs: %f %d\n\n", data, f, i);}evt_typedef(FoobarEvent, int, float, int);int main(int argc, char *argv[]) {    FoobarEvent foobar1 = {0,};    evt_define(foobar2, int, float, int) = {0,};        int data1 = 10;    int data2 = 20;    int data3 = 30;        evt_bind(foobar1, on_foobar, data1);    evt_bind(foobar1, on_foobar, data2);        evt_bind(foobar2, on_foobar, data3);        evt_signal(foobar1, 3.141f, 123);        evt_signal(foobar2, 2.718f, 666);    return 0;}


[Edited by - robinei on July 30, 2009 4:16:47 AM]
Quote:Original post by robinei
Here's code for an event system I made in C.

That's some heavy macroing, heh.
I'm not sure about this, but using your code, does it mean that every time I define an event in a different scope, it will generate a new struct for that event?
Quote:Original post by auron777
Quote:Original post by robinei
Here's code for an event system I made in C.

That's some heavy macroing, heh.
I'm not sure about this, but using your code, does it mean that every time I define an event in a different scope, it will generate a new struct for that event?

Every time you define an event using evt_define it will create a variable with a new anonymous type. However, if you use evt_typedef you can define event types once, beforehand.

This topic is closed to new replies.

Advertisement