Arrays of C++ Function Pointers & Lambdas
Imagine you’re building a game where the player has to complete different goals or objectives one at a time. Probably, the order of these goals is known at compile time of your app. Probably, it’s also known what kind of goals they are and what data they require at compile time. Now, one typically starts implementing such a scenario with a bunch of if-else logic, e.g. if the player is at goal number 5, check that she needs to score some points in less than some number of seconds. Obviously, this is not really scalable and you will notice that the respective function containing this huge if-else tree will become a giant monolythic block of code. Also, the memory for all those goals and required objects is allocated the entire time which is not really optimal given that the player plays only one goal at a time.
Wouldn’t it be great to instead only load goal 5 when it’s needed? The obvious first thing is to create goal objects. The second is that we’d need to store all these objects in an array. So we need a base type — IGoal — for this to work.
struct IGoal {
IGoal(int count) : _count(count) {}
protected:
int _count = 0;
};
struct GoalA : IGoal {};
struct GoalB : IGoal {};
template<int seconds>
struct GoalC : IGoal {
private:
int _seconds = seconds;
};
/*
const IGoal *goals[3] {
new GoalA(5),
new GoalB(10),
new GoalC<10>(100)
};
*/For simplicity, I avoided writing all the required constructors. Note that if we just create an array of goals that would mean still we’d allocate memory for all objects right away. Not good. Instead, we can create an array of function pointers. and invoke the respective function when setting the current goal. Because every goal object is different we need a template factory method which is responsible for instantiating a goal and returning a IGoal pointer.
template<typename T, int count>
IGoal *create() {
return new T(count);
}
IGoal *(* const goals[3])() {
&create<GoalA, 5>,
&create<GoalB, 10>,
&create<GoalC<10>, 100>
};This way we store ordered information about how to allocate the respective goal and which parameters to pass to the constructors during allocation. The only thing left is to remember which goal you’re currently at and add a function to set the current goal.
IGoal *_current_goal = nullptr;
void setCurrentGoal(int id) {
if (_current_goal) {
delete _current_goal;
}
// Check that id is in range
_current_goal = (*goals[id])();
}When inspecting the size of this array you will find that one function pointer requires 8 bytes if it was a global object. However, if this functionality is encapsulated in a class, which is a reasonable assumption in object oriented programming, you will find that the function pointer uses 16 bytes. That’s quite a lot. Also, due to the templating the comiler actually generates a separate method for each array entry. That will increase the executable size by a bit but fortunately, since C++11 we can use lambda expressions:
IGoal *(* const goals[3])() {
[]()->IGoal * { return new GoalA(5); },
[]()->IGoal * { return new GoalB(10); },
[]()->IGoal * { return new GoalC<10>(100); }
};This is the approach we’ve ended up using for the goal system in Shnips, our game for iOS and Android. It’s good to know that a pure lambda (without a capture) is just a stateless object with an overloaded () operator, it’s just 1 byte big. However, each lamda is a different object, even if it has the same signature, so we can’t have an array of pure lambdas. What we can do though, is assign it to a function pointer, which would allocate 8 bytes. It’s still an improvement because it halves the memory and reduces the executable size. If your lambda needs a capture (i.e. a reference to an object in the outer scope) there is no other way but assigning it to a std::function object, which will use 48 bytes and is generally slower to execute compared to raw function pointers. Here’s a comparison:
// sizeof(l0) = 1 byte
auto l0 = []()->int { return 42; };// sizeof(l1) = 8 bytes
int(*l1)() = []()->int { return 42; };// sizeof(l2) = 48 bytes
std::function<int()> l2 = []()->int { return 42; };// sizeof(l3) = 8 bytes
int func() { return 42; }
auto l3 = &func;
// sizeof(l4) = 16 bytes
class Class {
int func() { return 42; }
};
auto l4 = &Class::func;
This design pattern is quite powerful and mostly used to specify a list of command objects which are performed at a later stage of the program. Game engines like Unity and Unreal Engine use command buffers to model their rendering pipeline, Google’s TensorFlow uses a similar pattern to model operations in neural networks, etc. Of course, the pattern can be extended to dynamic command buffers by using a dynamic data structure like std::vector, std::queue or std::unordered_map. Happy coding!
