Skip to main content
GameDev.net gamedev.net
🔒 Locked

A C++ events implementation

Started by Bargaust Apr 3, 2009 at 3:35 AM 11 replies 2.3k views
Original Post
Bargaust
Bargaust
Hi all, I'm working on an events framework for C++. I'm trying to base it as closely on C#/.NET's implementation as possible. So far, I'm quite happy with how it is turning out, but it'd be nice to receive some feedback from those who are more knowledgeable on the C++ language. Here are the classes used:

#pragma once

#include <list>

// The base class for all event handlers
class EventHandler
{
	friend class Event;

	// Abstract methods
private:
	virtual void Invoke(void* sender) = 0;

	virtual EventHandler* Clone() = 0;
	virtual bool Equals(EventHandler& other) = 0;
};

// Implements an event handler for a method of a given type
template<class TTarget>
class MethodEventHandler : public EventHandler
{
	typedef void(TTarget::*methodType)(void*);

	// Fields
private:
	TTarget* target;
	methodType method;

	// Constructor
public:
	MethodEventHandler(TTarget* target, methodType method)
	{
		this->target = target;
		this->method = method;
	}

	// Methods
private:
	void Invoke(void* sender)
	{
		(this->target->*(method))(sender);
	}

	EventHandler* Clone()
	{
		EventHandler* clone = new MethodEventHandler(this->target, this->method);
		return clone;
	}

	bool Equals(EventHandler& other)
	{
		MethodEventHandler* evt = dynamic_cast<MethodEventHandler*>(&other);

		if (evt == NULL)
		{
			return false;
		}
		else
		{
			return ((this->target == evt->target) && (this->method == evt->method));
		}
	}
};

// Implements an event handler for a function
class FunctionEventHandler : public EventHandler
{
	typedef void(funcType)(void*);

	// Fields
private:
	funcType* func;

	// Constructor
public:
	FunctionEventHandler(funcType* func)
	{
		this->func = func;
	}

	// Methods
private:
	void Invoke(void* sender)
	{
		(this->func)(sender);
	}

	EventHandler* Clone()
	{
		EventHandler* clone = new FunctionEventHandler(this->func);
		return clone;
	}
	
	bool Equals(EventHandler& other)
	{
		FunctionEventHandler* evt = dynamic_cast<FunctionEventHandler*>(&other);

		if (evt == NULL)
		{
			return false;
		}
		else
		{
			return (this->func == evt->func);
		}
	}
};

// The main event class, which allows EventHandlers to be added, removed, and for the event to be invoked
class Event
{
	// Fields
private:
	std::list<EventHandler*> targets;

	// Constructors / Deconstructors
public:
	Event()
	{
	}

	~Event()
	{
	}

	// Methods
private:
	void Subscribe(EventHandler& evt)
	{
		EventHandler* evtClone = evt.Clone();
		this->targets.push_back(evtClone);
	}

	void Unsubscribe(EventHandler& evt)
	{
		std::list<EventHandler*>::const_iterator iter = this->targets.begin();
		while (iter != this->targets.end())
		{
			if ((*iter)->Equals(evt))
			{
				delete (*iter);
				iter = this->targets.erase(iter);
			}
			else
			{
				++iter;
			}
		}
	}

	void Invoke(void* sender)
	{
		for (std::list<EventHandler*>::const_iterator iter = this->targets.begin(); iter != this->targets.end(); ++iter)
			(*iter)->Invoke(sender);
	}

	// Operators
public:
	Event& operator +=(EventHandler& evt)
	{
		this->Subscribe(evt);
		return *this;
	}

	Event& operator -=(EventHandler& evt)
	{
		this->Unsubscribe(evt);
		return *this;
	}

	void operator()(void* sender)
	{
		this->Invoke(sender);
	}
};

Using the classes is very straightforward. Here is an example:


#include <iostream>
#include "Events.h"

class Counter
{
	// Fields
private:
	int count;

	// Constructor
public:
	Counter()
	{
		this->count = 0;
	}

	// Methods
public:
	void Increment()
	{
		this->count += 1;
		
		// Determine whether a multiple of ten has been reached
		int div10remainder = this->count % 10;
		if (div10remainder == 0)
			this->MultipleOfTen(this);
	}

	int GetCount()
	{
		return count;
	}

	// Events
public:
	Event MultipleOfTen;
};

void Counter_MultipleOfTen(void* sender)
{
	// Get a pointer to the counter that invoked the event
	Counter* counter = (Counter*)sender;

	// Output the count
	std::cout << "Multiple of ten found : " << counter->GetCount() << std::endl;
}

int main()
{
	// Create a counter
	Counter counter = Counter();

	// Add an event handler for the 'MultipleOfTen' event
	counter.MultipleOfTen += FunctionEventHandler(Counter_MultipleOfTen);

	// Count to a hundred
	for (int i = 0; i < 100; ++i)
		counter.Increment();

	// Remove the event handler
	counter.MultipleOfTen -= FunctionEventHandler(Counter_MultipleOfTen);

	// Wait for key press
	std::cin.get();
}

For simplicities sake, the example only demonstrates the FunctionEventHandler variation of EventHandler. MethodEventHandler is used in the same way, except you have to specify the type and instance of the target. If anyone needs an example of this, I'll post that too. I'm still working on this. I want to add EventArgs (again, a-la .NET) and tidy a few things up. Any feedback on this work-in-progress would be welcome. :) *** Quick question: can I use a slist in the version of STL supplied with VC++? I can't seem to find it anywhere. :( Thanks!
waZim
waZim
You can use std::list,, but unlike your "requirement" you need a singly linked list while std::list is doubly linked, if thats not a problem then go for it.

But i would recommend std::vector because std::vector is stored like arrays internally and its cache friendly plus you can do random access on it (with index)

And unlike arrays storage vectors is handled automatically, so you don't need to worry about that. it will expand and contract as needed.
Doggan
Doggan
slist isn't part of the C++ standard, and it's not part of VC++ STL's implementation. The STL list is a doubly linked list, but if you treat it like a singly linked list (i.e. only iterate in one direction), the performance shouldn't be different. Using a doubly linked list will obviously use more memory than a singly linked list implementation, since you need to store a pointer to the previous element. If this is actually a problem for you, it's simple enough to roll your own singly linked list implementation in 10 minutes ;)

Regarding your implementation: MultipleOfTen is of type Event. This means you could add MethodEventHandlers, FunctionEventHandlers, xxxEventHandlers to it. Was this your intent? I would think more 'true to C#' implementation would require the type of event:

i.e. Event MultipleOfTen

Then the event knows the exact signature of the functions it is operating on, eliminating the need for the void pointer, and thus giving you more type safety.
Doggan
Doggan
Quote:
Original post by waZim
But i would recommend std::vector because std::vector is stored like arrays internally and its cache friendly plus you can do random access on it (with index)

And unlike arrays storage vectors is handled automatically, so you don't need to worry about that. it will expand and contract as needed.


That's no good. An event system does not need random access. Plus frequent removing of callbacks (via -= operator) will be O(n) in a vector implementation, versus O(1) in a list implementation.
ToohrVyk
ToohrVyk
Quote:
Original post by Doggan
That's no good. An event system does not need random access. Plus frequent removing of callbacks (via -= operator) will be O(n) in a vector implementation, versus O(1) in a list implementation.
Without random access, you will spend O(n) time looking for the callback to be removed, even in a list.

On the other hand, if the order of callbacks is not important, removing from a vector is an O(1) task if you know what element you need to remove (assign the last element to that element, then pop).
Bargaust
Bargaust
Quote:
Original post by Doggan
Regarding your implementation: MultipleOfTen is of type Event. This means you could add MethodEventHandlers, FunctionEventHandlers, xxxEventHandlers to it. Was this your intent? I would think more 'true to C#' implementation would require the type of event:

i.e. Event MultipleOfTen

Then the event knows the exact signature of the functions it is operating on, eliminating the need for the void pointer, and thus giving you more type safety.


This is intentional. It is designed like this so both methods and functions can be attached as an EventHandler to the same event.

The void pointer you refer to is probably the sender parameter of the function. This is required, as it is in C#, and represents the object that invoked the event. In my example, this is an instance of Counter from which the count is ascertained and output.

Thanks for the info on slist - indeed, I only need forward traversal through the list so I guess std::list is the closest I'm going to get.

Thanks for your comments so far!
Zipster
Zipster
Check out this thread, and especially this article. There's a wealth of information. I'm personally using the system presented in the article and really like it so far. I had to modify it a bit to take out typeid's and allow multiple handlers per event, but those are fairly straightforward changes.
Bargaust
Bargaust
Quote:
Original post by Deyja
What does this implementation offer that boost::signals does not?

I'm not sure - I don't know much about that particular implementation. You tell me. :p

I've added event arguments, and also template-ised the type of the sender (so no more void*).

Here is the latest events code:
#pragma once#include <list>// The base class for all event handlerstemplate <class TSender, class TEventArgs>class EventHandler{	template <class TSender, class TEventArgs>	friend class Event;	// Abstract methodsprivate:	virtual void Invoke(TSender* sender, TEventArgs e) = 0;	virtual EventHandler* Clone() = 0;	virtual bool Equals(EventHandler& other) = 0;};// Implements an event handler for a method of a given typetemplate<class TSender, class TTarget, class TEventArgs>class MethodEventHandler : public EventHandler<TSender, TEventArgs>{	typedef void(TTarget::*methodType)(TSender*, TEventArgs);	// Fieldsprivate:	TTarget* target;	methodType method;	// Constructorpublic:	MethodEventHandler(TTarget* target, methodType method)	{		this->target = target;		this->method = method;	}	// Methodsprivate:	void Invoke(TSender* sender, TEventArgs e)	{		(this->target->*(method))(sender, e);	}	EventHandler* Clone()	{		EventHandler* clone = new MethodEventHandler(this->target, this->method);		return clone;	}	bool Equals(EventHandler& other)	{		MethodEventHandler* evt = dynamic_cast<MethodEventHandler*>(&other);		if (evt == NULL)			return false;		else			return ((this->target == evt->target) && (this->method == evt->method));	}};// Implements an event handler for a functiontemplate <class TSender, class TEventArgs>class FunctionEventHandler : public EventHandler<TSender, TEventArgs>{	typedef void(funcType)(TSender*, TEventArgs);	// Fieldsprivate:	funcType* func;	// Constructorpublic:	FunctionEventHandler(funcType* func)	{		this->func = func;	}	// Methodsprivate:	void Invoke(TSender* sender, TEventArgs e)	{		(this->func)(sender, e);	}	EventHandler* Clone()	{		EventHandler* clone = new FunctionEventHandler(this->func);		return clone;	}		bool Equals(EventHandler& other)	{		FunctionEventHandler* evt = dynamic_cast<FunctionEventHandler*>(&other);		if (evt == NULL)			return false;		else			return (this->func == evt->func);	}};// The main event class, which allows EventHandlers to be added, removed, and for the event to be invokedtemplate <class TSender, class TEventArgs>class Event{	// Fieldsprivate:	std::list<EventHandler<TSender, TEventArgs>*> targets;	// Constructors / Deconstructorspublic:	Event()	{	}	~Event()	{	}	// Methodsprivate:	void Subscribe(EventHandler<TSender, TEventArgs>& evt)	{		EventHandler<TSender, TEventArgs>* evtClone = evt.Clone();		this->targets.push_back(evtClone);	}	void Unsubscribe(EventHandler<TSender, TEventArgs>& evt)	{		std::list<EventHandler<TSender, TEventArgs>*>::const_iterator iter = this->targets.begin();		while (iter != this->targets.end())		{			if ((*iter)->Equals(evt))			{				delete (*iter);				iter = this->targets.erase(iter);			}			else			{				++iter;			}		}	}	void Invoke(TSender* sender, TEventArgs e)	{		for (std::list<EventHandler<TSender, TEventArgs>*>::const_iterator iter = this->targets.begin(); iter != this->targets.end(); ++iter)			(*iter)->Invoke(sender, e);	}	// Operatorspublic:	Event& operator +=(EventHandler<TSender, TEventArgs>& evt)	{		this->Subscribe(evt);		return *this;	}	Event& operator -=(EventHandler<TSender,TEventArgs>& evt)	{		this->Unsubscribe(evt);		return *this;	}	void operator()(TSender* sender, TEventArgs e)	{		this->Invoke(sender, e);	}};


And an updated example - this time demonstrating MemberEventHandler too. :)

#include <iostream>#include "Events.h"class Counter{	// Fieldsprivate:	int count;	int numMultiplesOfTen;	// Constructorpublic:	Counter()	{		this->count = 0;		this->numMultiplesOfTen = 0;		this->MultipleOfTen += MethodEventHandler<Counter, Counter, int>(this, &Counter::This_MultipleOfTen);	}	~Counter()	{		this->MultipleOfTen -= MethodEventHandler<Counter, Counter, int>(this, &Counter::This_MultipleOfTen);	}	// Methodspublic:	void Increment()	{		this->count += 1;				// Determine whether a multiple of ten has been reached		if ((this->count % 10) == 0)			this->MultipleOfTen(this, this->count);	}	int GetCount()	{		return this->count;	}	int GetNumMultiplesOfTen()	{		return this->numMultiplesOfTen;	}	private:	void This_MultipleOfTen(Counter* sender, int num)	{		this->numMultiplesOfTen += 1;	}	// Eventspublic:	Event<Counter, int> MultipleOfTen;};void Counter_MultipleOfTen(Counter* counter, int num){	// Output the count	std::cout << "Multiple of ten found : " << counter->GetCount();	std::cout << " (" << counter->GetNumMultiplesOfTen() << " found so far)" << std::endl;}int main(){	// Create a counter	Counter counter = Counter();	// Add an event handler for the 'MultipleOfTen' event	counter.MultipleOfTen += FunctionEventHandler<Counter, int>(Counter_MultipleOfTen);	// Count to a hundred	for (int i = 0; i < 100; ++i)		counter.Increment();	// Remove the event handler	counter.MultipleOfTen -= FunctionEventHandler<Counter, int>(Counter_MultipleOfTen);	// Wait for key press	std::cout << "Press Enter to exit";	std::cin.get();}


I'm pretty happy with the state of this at the moment.

The only niggle that I've yet to solve is how to now say that Event is a friend of the EventHandler class? As you can see above, I've have to comment out the declaration and make the methods public, but I'd rather they be hidden away as nothing other than Event should use them. Anyone know how I can do this? - Nevermind. Fixed and updated.

Again, all feedback is welcome. :)

[Edited by - Bargaust on April 3, 2009 9:10:28 AM]
Antheus
Antheus
Vector vs. list is a tricky issue, and neither is superior from start.

The issue that occurs is how to handle callbacks which modify subscriptions. It may be their own, it may be others.

- Callback removes itself. How does callback iteration code handle that, do any instances get de-allocated? Non-issue in managed languages, big hassle in C++
- If callback removes itself, the current iterator may be invalid. Storing 'next' doesn't really help since:
- Callback modifies other elements. If current callback removes next element, then previously stored next element is invalid.
- Life-cycle issues. What happens if an callback object is deleted? Again, issue that doesn't exist in managed languages. The basic solution to this is signal/slot approach, where targets keep memory managed slots.

For synchronous callbacks, plenty of issues are solved can be solved by keeping two lists:
- one list holds callbacks (cbs)
- other lists holds modifications to previous list (delta)
When a subscription is either made or removed, instead of applying it immediately, it's instead put into delta list.
When triggering an event, delta list (if not empty) is applied to 'cbs', then cbs is iterated over.

Since callbacks are dispatched synchronously, no matter how they call each other, at some point they will complete, but the callbacks issued will apply only to the "snapshot" of the callback graph at the time original event was fired. This may result in unintuitive results, if some callback will rely on being able to modify current iteration, but IMHO, it solves more problems than it introduces.

Second problem applies to non-trivial callbacks. Assume you have a timer which issues a callback once every 30 minutes. While observers only hook the callback, the callback will need to be fired when subscription is made, not just when the timer ticks. Common use case: an UI component is connected to timer object, but doesn't update UI since it's still 27 minutes till next tick, and given event-model, user has no way of getting status of timer besides the event callback.

To solve this, either special type of events is needed, or events need to provide on_subscribe/on_unsubscribe actions. How exactly depends on actual API.

And that's not even touching concurrent systems which are a big huge mess.
Bargaust
Bargaust
I don't imagine there are many situations in which a callback would attempt to remove itself, but yes would currently cause it to break since the iterator can't be moved forward.

As for lifetimes of callback objects, surely its just a case of cleaning up after yourself? If an object is deleted, but make sure you remove the event handlers associated with it beforehand. If you don't, yes it'll crash.

I'm don't really understand what you mean in your other comments. I may have been away from C++ for too long! Thanks for the comments. :)
Doggan
Doggan
Quote:
Original post by ToohrVyk
Quote:
Original post by Doggan
That's no good. An event system does not need random access. Plus frequent removing of callbacks (via -= operator) will be O(n) in a vector implementation, versus O(1) in a list implementation.
Without random access, you will spend O(n) time looking for the callback to be removed, even in a list.

On the other hand, if the order of callbacks is not important, removing from a vector is an O(1) task if you know what element you need to remove (assign the last element to that element, then pop).


The O(n) search through the list is inevitable. A vector implementation will incur an additional O(n) removal operation due to the fact that a vector is a contiguous block of memory, whereas linked lists are constant time removal. I don't see how you could always guarantee that the next function pointer to be removed will be at the end of the vector. Functions are added to event callbacks in any order, and removed in any order, which is why I say the initial O(n) search is inevitable.
Zahlman
Zahlman
Quote:
Original post by Doggan
If this is actually a problem for you, it's simple enough to roll your own singly linked list implementation in 10 minutes ;)


Not one of comparable quality/functionality to std::list.

Quote:
The O(n) search through the list is inevitable.


That was his point.

Quote:
A vector implementation will incur an additional O(n) removal operation due to the fact that a vector is a contiguous block of memory, whereas linked lists are constant time removal.


O(n) + O(n) = O(n). We don't care about constant factors here. (They may anyway be signal lost in the noise, since e.g. traversing a std::vector is typically faster than traversing a std::list even though they're both O(n) - due to reasons of cache coherency and so forth.)

Quote:
I don't see how you could always guarantee that the next function pointer to be removed will be at the end of the vector.


You can't, but you don't necessarily care. If there is no need to preserve the order of existing elements, an element can be removed from std::vector in O(1) time, by replacing it with the last element (copying the last element over and then calling .pop_back(), for example). Further, any number of elements matching some criteria can be removed from std::vector in a single pass in O(n) time using std::remove{_if}.

Topic Locked

This topic has been locked by a moderator. New replies are not allowed.

Sign in to reply to this topic.