Quick and Easy Delegation in C++ Game Development

Delegation is a term often used in computer science, but since it has so many different meanings, talk about them usually becomes very confusing. That is why I have been so specific with my title. What I'm about to show you is not an interface or design pattern, but an implementation of an object that can be delegated to call a given function. This responsibility is often also given the title function binding.

A number of resources exist on this already (including std::function) but many of them involve heap allocation or clunky reflection systems that can hurt performance and add to code complexity. Instead, let's focus on a subset of functionality - the part that assists us immensely in game development.

Overview

A delegate has 2 responsibilities:

  • set the delegate
  • call the delegate

To this degree, think of a delegate as a slot for your functions. It can hold onto any single function (or no function) to be called at will. In C, all we have are function pointers, which act as decent delegates:

void func(void) {  
    printf("Hello!\n");
}

void (*FunctionPointer)(void) = func;  
FunctionPointer(); // Will print "Hello!"  

At first glance, this seems like a perfect solution (and a lot can be done with C function pointers). We can store whatever function we want and call it later! It is here however that we notice 2 big problems:

  • What if I want different function signatures?
  • I'm writing C++, What if I want to use methods?

Let's tackle these one by one.

Different Signatures

A function's signature is its definition. For example, void func(void) is a function that takes void and returns void. In C, a function pointer's type is determined by its full signature, including the return value. Therefore, if we want to write a more generic delegate, we're either going to have to write some crazy template code, or make some reasonable compromises.

Return Types

When talking about return types, it's best to bring in the context of our delegate. In game development, we mostly use delegates to notify game objects when certain things have happened. As a contrived example, to let an object know when it is in a collision:

function EnemyOnCollision() {  
    this.m_Health -= 10;
}

class GameObject {  
    delegate OnCollision;
    Collider m_Collider;
}

GameObject obj;  
obj.OnCollision = EnemyOnCollision;

...

foreach(collider : m_ColliderList) {  
    if(collider.InCollision())
        collider.Owner.OnCollision();
}

In this psuedo-code, we can set the OnCollision delegate of a game object. When it collides, that delegate will be called and our logic will be run.

Notice the nature of the delgate. Since we are using it to notify, we really don't care about the return type. Therefore, for the purposes of this article, we will make all delegated functions return nothing (void). As big a fan I am of extensibility and elegance, believe me when I say I did not like coming to this conclusion. However, so many problems with delegation become automatically solved when not having to worry about a return value (this is the Quick and Easy guide to delegation after all).

Parameter Types

When working on function binding, many programmers (myself included) have tried to write an all-encompassing monolithic system for storing and calling functions arbitrarily. While a great exercise, this is of such little use in a game engine, and it often contributes more to code complexity than it does to functionality.

Like return values, parameter types make a delegate immensely more complex. With parameter types, the goal is to store the types as template parameters in a static function that can be used to parse the real parameters back out of an arbitrary block of data:

template <typename P1, typename P2>  
static void trampoline(void(*fn)(void), void const *data) {  
    // Convert the function to the correct type
    typedef void (*converted_t)(P1, P2);
    converted_t conv = reinterpret_cast<converted_t>(fn);

    // Retreive the parameters from the data
    char *param_base = reinterpret_cast<char const *>(data);
    P1 *param1 = reinterpret_cast<P1 *>(param_base);
    P2 *param2 = reinterpret_cast<P2 *>(param_base + sizeof(P1));

    // Call the actual function
    conv(*param1, *param2);
}

This function takes a type-erased function void(*fn)(void), casts it to a type with the right parameters, and calls it with pointers dereferencing into the passed-in block of data containing the parameters.

The neat thing about this function is that it can store type information in a static C-style function using the template paramters. We'll be using that later.

The less- neat part is that this function needs to be written once for each number of parameters you want to accept. Or, with crazy template meta-programming, you can reduce it down to one function (with 4 helper-structs and a helper function in my implementation), but in the end, is any of it really worth it?

One of the problems of completely erasing type in a delegate comes from the calling end. As the user of this code setting a delegate, you now need to know the exact parameters the caller will be passing, and their order!

Can't we just resort to the C-style days of passing around blocks of void* data? We certainly can, but we can also do even better. We can deduce the type of the struct parameter while setting the delegate, and automatically cast it to that same type when it gets called:

struct data {  
    float time;
    int health;
}

void action(data const *) {...}

delegate d;  
d.set(action);

...

data game_data;

// This takes a void pointer, but casts it before calling so the function doesn't notice a difference.
d.call(&game_data);  

This means no top-level void pointers, and the order of the parameters is still the compiler's responsibility!

Methods

We seem to have a good grasp now over how we want to set up the delegate. However there is still one more monkey wrench in the mix: member functions. Also known as methods, member functions can't be stored in the same way as functions. Instead, we have to refer to them as ReturnType (ClassType::*MethodPointer)(Params...). To make matters worse, function pointers and member function pointers aren't even defined as having the same size!

There are many ways to go about fixing this problem. One method takes advantage of an important fact about methods: the value of a member function pointer is always known at compile-time. Because of this, member function pointers can be passed into templates by value:

struct foo {  
    void bar(void);
};

template <void (foo::*method)(void)>  
void passMethod();

passMethod<&foo::bar>();  

Here however, we take one step forward and two steps back. While member function pointers can be passed as template arguments, they also need to know the class type before passing them. So we would write:

template <typename T, void (T::*method)(void)>  
void passMethod();

passMethod<foo, &foo::bar>();  

I have gone down this road before... and it's worked! Perfectly actually. But I believe semantics are just as important as the rest of code design, and this function call is really ugly. In a perfect world, I want to type at most one template parameter into a function call, and preferably none. I'd want the template to be deduced at compile time.

To do this, like for any parameter, we can pass it into the function itself:

template <typename T>  
void passMethod(void (T::*method)(void));

passMethod(&foo::bar);  

Unfortunately, this ruins the ability to store the member function pointer as a template argument, and there is no way to also get the compiler to deduce the type of a value passed into the template. Previously, I said that storing member pointers as template parameter data was nice, but I didn't say it was necessary. The delegate we'll be writing chooses to store the actual member function pointer in order to allow for type deduction and better semantics.

Implementation

Alright. Until now, I've been outlining the problem I intend to solve, a very important step in engineering. But you've made it this far, which means we can actually implement the delegate now. We want our delegate to be handle both methods and static functions, taking a constant pointer (we don't intend on modifying the parameters, right?) to an arbitrary data-type and returning void. Now that we've zoomed in on a specific problem to solve, things actually become pretty simple.

To start my implementation, it is clear that I will need to store the member functions and static functions in different ways. Since there can only ever be one function in the delegate, why not store these as a union?

struct method_storage { ... }  
struct function_storage { ... }

union storage {  
    method_storage as_method;
    function_storage as_function;
};

This way, we can store whatever we need for either in a more compact space.

Second, we now need to actually store these functions. No matter how you look at it, this is going to take a lot of reinterpret_cast-ing. The static function is simple enough. Just cast it to void (*)(void const *) and cast the parameter type back in later:

struct function_storage {  
    void (*function)(void const *);
};

Methods become a bit harder. We can reinterpret_cast between member pointer types, but that means we need a class to cast to. There is no void class, so instead, we make one:

// This class exists solely to be cast to and from.  
struct dummy_class {  };

// Store the method in terms of the dummy class, and cast them back later.
struct method_storage {  
    dummy_class *object;
    void (dummy_class::*method)(void const *);
};

Third, since we're erasing as much information as we can about the function, when we call the delegate, we actually need to remember if we were dealing with a method or function. While we could have some sort of boolean and an if-check, we can also implement the entire call without branching.

Note: This is a very low-level optimization, but it means an increased chance of the compiler inlining the entire delegate call, which would give our delegate calls the same performance as calling a regular function. We really want this.

To do this, I decided to store an extra pointer to an "entry function." This function is either call_function or call_method. When I invoke the entry-point function pointer, it treats the storage as it needs and calls its function correctly. I also added a call_nothing for clearing out the delegate. Again, this is just possibly more performant than checking some flag.

On a trickier note now, these entry-point functions are where I decided to keep my type parameters. For static functions, it's just the parameter type, but I also store the class type for my methods.

class delegate {  
    union storage { ... };

    template <typename ClassType, typename DataType>
    static inline void call_method(delegate const *d, param_t data) {
        ClassType *obj = reinterpret_cast<ClassType*>(d->storage.as_method.object;
        typedef void (ClassType::*calling_t)(DataType const *);
        calling_t method = reinterpret_cast<calling_t>(d->storage.as_method.method;
        DataType const * param = reinterpret_cast<DataType const *>(data);

        (obj->*method)(param);
    }

    template <typename DataType>
    static inline void call_function(delegate const *d, param_t data) {
        typedef void (*calling_t)(DataType const *)
        calling_t func = reinterpret_cast<calling_t>(d->storage.as_function.function)
        DataType const * param = reinterpret_cast<DataType const *>(data);
        func(param);
    }

};

Last, but not least, we need a way to actually set the delegate. We have a way to store them, but now we have to write the functions that do the type deduction and setup. Again, for static functions, this is easy:

template <typename DataType>  
void set(void (*func)(DataType const *)) {  
  function_delegate &del = storage.as_function;

  del.function = reinterpret_cast<void (*function)(void const *)>(func);

  entry_function = call_function<DataType>;
}

Now, suprisingly, the method one isn't too difficult. It's the exact same concept, except we have to pass the object to call, and we can deduce the class type from that:

template <typename ClassType, typename DataType>  
void set(ClassType *obj, void (ClassType::*method)(DataType const *)) {  
    method_delegate &del = storage.as_method;

    del.object = reinterpret_cast<dummy_class *>(obj);
    del.method = reinterpret_cast<void (dummy_class::*)(void const *)>(method);

    entry_function = call_method<ClassType, DataType>;
}

Finally, to round out the implementation, we write the actual call method and clear, a way to empty out the current state of the delegate:

inline void call(void *data) {  
    entry_function(this, data);
}

void clear() {  
    // The call_nothing function is seriously just {  }
    entry_function = call_nothing;
}

Wrap-Up

There you have it! An implementation of a super-performant, real-time-oriented function delegate. It does no heap allocation, isn't templated (on the outside), and can be stored easily in containers. Now it can be used by itself, or incorporated into an event system (more on that later). Either way, delegates provide a great layer of indirection between an action that needs to be taken and the object responsible for invoking that action.