Saturday, August 31, 2013

Delegates in C++ - Weekend Edition

I discovered delegates while working in C#, which supports them natively.  Unfortunately, C++ doesn't handle them so elegantly.

Basically, sometimes it would be nice to "subscribe" to certain events or messages in a system. For example, it might be nice to get a callback when a particular object is destroyed so you can clean up any references you have to it. I ran into this particular problem when I had an "enemy" object holding a pointer to the "player" object - this caused a crash when the player died and their object was destroyed.

There are ways to handle this, though most of them are inelegant or require the object generating the event to know more about the receiver than we would like (say, a common base class that the receivers can derive from).  This is also fairly straightforward if you just want to call a static function, but what if you want to call a member function?

The main problem is that C++ just really doesn't let you easily create a collection of completely unrelated types, and pointers to member functions in unrelated classes are basically completely unrelated.

C# handles this quite nicely by declaring a delegate - basically, a prototype for the callback function - and storing references to the object/function in a list.  When you "call" the list of functions, every object/function in the list receives the callback with the arguments you provide.

There are a lot of attempts to do this in C++.  C++11 may improve things.  I just went and rolled my own, taking ideas from here and there and trying to keep the code as simple as possible.  It involves a few macros to wrap some complexity, but seems to work.


 The header file that makes this all work is down at the bottom.  First, how it's used.

In the calling class, you need declare the event handler, specifying what gets passed to the receiver:
class cSender
{
public:
 cEventHandler<cGameObject *> NotifyDestroy;
};

Note that my implementation only supports receiver functions that have a single argument - though if you need to pass more, store them in a class and construct an instance of it to pass to the event handler.

Then you need to declare the target function that matches the sender (argument type, receiving class name, and receiving function):

class cReceiver
{
public:
 EVENT_FN(cGameObject *, cReceiver, OnDestroy);

};

void cReceiver::OnDestroy(cGameObject *obj)
{
 printf("Bang! %p\n", obj);
}


And then you need to add the target function to the sender:

void cReceiver::SomeFunction(cSender *sender)
{
 sender->NotifyDestroy += EVENT_HANDLER(cGameObject *, OnDestroy);

}


Then in the sender, you can call NotifyDestroy(obj) with the object that was destroyed, and every class that added their event handler will get the callback.

Internally, the event handler works by keeping a list of static function pointers, and a void * to store the target class "this" pointer.  The static functions are created by the EVENT_FN macro, and just redirect to the correct member function.  These have to be static functions, because despite coming from different classes, the C++ compiler considers them to have the same signature and you can store a collection of them easily.

It seems like a bit of a hack, but this sort of redirection-to-class is done all the time in other callback situations - when you spawn a new thread for instance.


The header file that makes all of this work is below (Disclaimer: None of the code here has been properly tested.  I modified this code to use STL instead of my own containers.)


//Contains information about a static function to call and user data to pass to it
template <typename T>
class cEventDelegate
{
 typedef void (*EventFn)(void *, T);
public:
 cEventDelegate(EventFn fn, void *user) : mFn(fn), mUser(user) { } 
 void operator()(T data) const { mFn(mUser, data); }
 bool operator==(const cEventDelegate &b) const { return mFn == b.mFn && mUser == b.mUser; }

private:
 EventFn mFn;
 void *mUser;
};

//Tracks multiple function calls for an event
template <typename T>
class cEventHandler
{
public:
 typedef cEventDelegate<T> tDelegate;
 typedef std::vector<tDelegate> tDelegates;

 cEventHandler() { }

 cEventHandler &operator +=(const tDelegate &target) { if(mFn.find(target) == mFn.end()) mFn.push_back(target); return *this; }
 cEventHandler &operator -=(const tDelegate &target) { mFn.erase(mFn.find(target)); return *this; }

 void operator()(T t)
 {
  for(tDelegates::const_iterator it = mFn.begin(); it != mFn.end(); ++it)
  {
   (*it)(t);
  }
 }
 
private:
 tDelegates mFn;
};

//Declares an event redirection from a static member function to a member function
#define EVENT_FN(T, cls, id) \
static void __Redirect_##id(void *c, T t) \
{ \
 cls *_this = (cls *)c; \
 _this->id(t); \
}; \
void id(T t) \
//--End macro-- --Intentionally blank--

//Construct an event handler from a member function
#define EVENT_HANDLER_EX(T, cls, id) cEventHandler<T>::tDelegate(cls->__Redirect_##id, cls)
#define EVENT_HANDLER(T, id) EVENT_HANDLER_EX(T, this, id)

No comments:

Post a Comment