This post is, as the title appropriately suggests, about the implementation of a simple event system for use in C++ applications, which is a fancy name for a list of function/method pointers. The main purpose of an event system is to have some code in a part of the application (module) be called whenever an event in another module takes place. An event system is a great tool for increasing code modularity and reducing dependencies and unnecessarily complex class hierarchies.
The simple event system described here uses Don Clugston's Fastest Possible C++ Delegates. Clugston's delegates are nice since they work on a wide range of compilers, fast, their syntax is nice, and allow you to use free functions as well as member functions. We could use boost::function and boost::bind instead, but using them incurs an additional dynamic memory allocation per callback object instance that we could do without, as well as introducing a dependency on boost, which is a HUGE library.
Instead of basing our approach on a vector of delegates, we'll use a list of delegates. Using a list rather than a vector allows us to remove a callback from an event in constant time given a "handle" to the callback in the event's callback list. Another important advantage to using a list is that we can easily order the callbacks in an event.
The event system we're after isn't complicated and nothing fancy is going on here, which I think is a good thing. We'll start with the example "client" code instead of the event system itself.
Client code:
// Demo application for the simple C++ event system.
// Visit code-section.com for the corresponding blog post.
# include <iostream>
# include <vector>
# include <Angel3D/Event.h>
using namespace std;
using namespace A3D;
struct Document
{
int fiddle, dee, dum; // document data...
};
// We use the DECLARE_EVENT macro to declare an event. The first argument is the return type.
DECLARE_EVENT( bool, Document*, const char* ) eventSaveDocument;
void SaveDocument( Document& doc, const char* sFileName )
{
cout << __FUNCTION__ << endl;
// ...
// code which saves the document here
// ...
// Now that the document has been saved,
// call all callbacks of the event (aka fire the event).
TRIGGER_EVENT( eventSaveDocument, &doc, sFileName );
// Alternatively, the following syntax can be used, which is necessary if we need to
// inspect the return values from the callbacks. A slightly different syntax is needed
// to account for callbacks possibly removing themselves (see the TRIGGER_EVENT macro definition).
/*for( HCALLBACK hCB = eventSaveDocument.GetFirstCallback();
hCB != eventSaveDocument.GetEndCallback();
hCB = eventSaveDocument.GetNextCallback( hCB ) )
{
if( false == eventSaveDocument.GetDelegate( hCB )( &doc, sFileName ) )
cout << "A callback is reporting an error!" << endl;
}*/
}
// A test callback function that we want to be called when a document is saved.
bool OnSaveDocumentFunction( Document* pDoc, const char* sFileName )
{ cout << __FUNCTION__ << endl; return true; }
class SomeClass
{
public:
bool OnSaveDocumentMethod( Document* pDoc, const char* sFileName )
{
cout << __FUNCTION__ << endl;
return true;
}
int x;
} g_someClass;
int main()
{
eventSaveDocument.InsertCallback( OnSaveDocumentFunction );
//eventSaveDocument += OnSaveDocumentFunction; // Same effect.
HCALLBACK hCB = eventSaveDocument.InsertCallback( MakeDelegate( &g_someClass, &SomeClass::OnSaveDocumentMethod ) );
Document doc;
SaveDocument( doc, "file.dat" ); // This will result in the two added callbacks to be called.
// Remove the first callback. We use the function pointer itself to
// identify the callback object in the list.
eventSaveDocument.RemoveCallback( OnSaveDocumentFunction );
// Remove the second callback using the handle we stored earlier.
//eventSaveDocument.RemoveCallback( hCB );
eventSaveDocument -= hCB;
return 0;
}
Of course, __FUNCTION__ is replaced by the preprocessor with the name of the function in which it occurs. This macro is quite useful for tracing and debugging purposes. The above code demonstrates using the event system. Notice the #include "Event.h" above. This is where we will write our awesome event system. The interface is simple and easy to use. It makes you want to use events all over your application, doesn't it? Now, on to Event.h.
# ifndef EVENT_H
# define EVENT_H
# include "FastDelegate/FastDelegate.h"
# include <list>
using namespace fastdelegate;
namespace A3D
{
/// Defines a generic callback list type.
typedef std::list< DelegateMemento > CallbackList;
/// Defines a handle type used for identifying a callback in an event.
typedef CallbackList::iterator HCALLBACK;
template <class Delegate>
class AEvent
{
protected:
CallbackList m_callbacks;
public:
typedef Delegate DelegateType;
/// Returns the list of callbacks for iteration. Use the INVOKE_CALLBACK macro
/// if you don't need to inspect return values.
CallbackList& GetCallbackList() { return m_callbacks; }
/// Inserts a delegate (callback) at the end of the list.
HCALLBACK InsertCallback( typename Delegate::type dlgt )
{ return m_callbacks.insert( m_callbacks.end(), dlgt.GetMemento() ); }
/// Returns a delegate from a callback handle, ready for invocation.
DelegateType GetDelegate( HCALLBACK hCB )
{ DelegateType dlgt; dlgt.SetMemento( *hCB ); return dlgt; }
/// Removes the callback identified by the specified callback handle (no search required).
void RemoveCallback( HCALLBACK hCB ) { m_callbacks.erase( hCB ); }
/// Searches for and removes the specified callback. A search is needed to find it in the callback list.
bool RemoveCallback( typename Delegate::type dlgt )
{
for( HCALLBACK cb = m_callbacks.begin(); cb != m_callbacks.end(); cb++ )
{
if( (*cb).IsEqual( dlgt.GetMemento() ) )
{
m_callbacks.erase( cb );
return true;
}
}
return false; // the specified delegate was not found.
}
HCALLBACK operator += (typename Delegate::type dlgt ) { return InsertCallback( dlgt ); }
void operator -= (HCALLBACK hCB ) { RemoveCallback(hCB); }
void operator -= (typename Delegate::type dlgt ) { RemoveCallback(dlgt); }
HCALLBACK GetFirstCallback() { return m_callbacks.begin(); }
HCALLBACK GetNextCallback( HCALLBACK hCB ) { HCALLBACK hRet = hCB; return ++hRet; }
HCALLBACK GetPrevCallback( HCALLBACK hCB ) { HCALLBACK hRet = hCB; return --hRet; }
HCALLBACK GetLastCallback() { return m_callbacks.back(); }
HCALLBACK GetEndCallback() { return m_callbacks.end(); }
};
# define DECLARE_EVENT( retType, ... ) A3D::AEvent< FastDelegate<retType(__VA_ARGS__)> >
/// This macro is a typing shortcut. The code goes through the list of callbacks in the event
/// and calls them. This macro is not suitable if the client needs to inspect the return value
/// from the callbacks, or call the callbacks in reverse order for example.
/// The iteration takes into account the scenario where a callback removes itself from the event.
# ifndef TRIGGER_EVENT
# define TRIGGER_EVENT( theEvent, ... ) \
for(A3D::HCALLBACK hCB = theEvent.GetCallbackList().begin(); hCB != theEvent.GetCallbackList().end(); )\
{ A3D::HCALLBACK hCurCB = hCB++; theEvent.GetDelegate( hCurCB )(__VA_ARGS__); }
# endif
}; // namespace A3D
# endif // inclusion guard
And there you have it, a simple and functional event system. One obvious caveat with this approach has to do with the implications of using a plain old std list container for storing callbacks. This adds some processing and memory penalty when adding and removing callbacks to events. This can be rather easily resolved by using a pooled list of some sort, especially since all callback objects for all sorts of events use the same object (only the Event class is templated, not the callback list and handle classes). But for most purposes, this performance penalty is nothing to worry about, so we'll leave this pooling optimization for some other time.
I hope this was helpful to someone. If you have any thoughts, corrections, questions, or suggestions, feel free to share them.
I realize all of this is poorly explained and worded, and I hope to refine it in the future. If you have any feedback, post a comment.
¶ | Used tags: cpp, programming