Back when I was writing a terrain generator in C++, I used this library called glConsole to put a quake-style dropdown console into the project. By a “dropdown console”, I mean the thing in the screenshot above - a place to execute scripts from inside the game. Basically, if I typed this in the console:
wireframe 1
I wanted to execute this in C++:
void Renderer::setWireframeRendering(bool value) {
this->wireframe = value;
}
However, this wasn’t possible in glConsole
. While it did most things pretty well, the one thing it could not do was bind member functions - functions which are part of a class. You could bind free functions and static functions, but not methods. Lately I’ve been working on a very simple first-person shooter using C++ and no game engine. I wanted something like glConsole
, but without the glaring downside. Plus, I was writing the game from scratch for a reason - why not write my own console?
In this post, I’ll explain my approach to implementing a callback system which stores functions and executes them at a later date. My goals for this system were:
- Member functions should be supported.
- You should be able pass in any method/callable object to be called at a later date.
- It should be (at least somewhat) typesafe.
Overview
Our goal is to implement a class, CallbackMap
, which has two responsibilities:
- Given a function name and a callable object, it will store a mapping between the two.
- Given a function name and some arguments, it will call the object associated with the function name, passing the given arguments.
The interface could look a bit like this:
Class CallbackMap {
public:
void storeCallback(const std::string& functionName, Callback function);
void call(const std::string& functionName, const std::string& args);
private:
std::unordered_map<std::string, Callback> callbackMap;
}
Remember we’re mainly interested in implementing a console. That means all of our “function calls” from the console will just be strings, which is why args
above is a string. We’ll assume args
is a space-delimited set of arguments; the input string (e.g. “foo 1 2 3
”) can simply be split at the first space and then passed into call
.
std::function
Our first job is to define Callback
, the actual “function” which we want to store and execute later. The first place your mind might turn when thinking about storing functions is a function pointer. However, this is exactly the mistake glConsole
made - using function pointers will not allow member functions to be specified as callbacks. Intuitively, this is because a member function is called upon a specific object, and normal C function pointers aren’t built to hold state like that.
We could write our own wrapper object to hold callables (e.g. by storing the function pointer along with a void*
), but luckily for us there’s already one in the standard library: std::function
. std::function
allows us to store anything that looks like a callable object: lambdas, functors, or free functions. For example:
int add(int a, int b) {
return a + b;
}
// later
std::function addFunc(&add);
std::cout << addFunc(1, 2); // prints 3
Member functions require a small trick to be stored. There’s two pieces to this trick. First, we’ll need to explain std::bind
, a function also introduced in C++11.
It takes, as arguments, a function and some arguments to that function. Yes, you read that right; some if its arguments are arguments to another function. It wraps the underlying function and the arguments for later use in a std::function
. When we call the returned std::function
, the arguments we passed in std::bind
are automatically forwarded to the underlying function. If we didn’t pass arguments, we need to specify them in the call. An example probably helps here:
void main () {
std::function addTwo = std::bind(&add, 2, std::placeholders::_1);
std::cout << addTwo(1); // prints 3
std::cout << addTwo(9000); // prints 9002
}
We pass the function add
to std::bind
, along with two parameters. These two parameters are the parameters the new std::function
will forward to add
. The first parameter, a
, we specify simply as 2
, and the second parameter, b
, we leave unspecified by passing a std::placeholders
object. Then, later, when we call the returned std::function
, we only need to pass one argument, b
- the 2
we passed in std::bind
is automatically passed to add
.
It’s kinda unclear how this helps us at all with member functions. What you might not realize is that, internally, C++ treats the object on which a member function is being called as an argument. That means if you take a call like this:
renderer.setWireframeRendering(true)
Internally, it really looks more like this:
setWireframeRendering(&renderer, true)
That means std::bind
can be used to bind specific object references to member functions like this:
Renderer renderer;
std::function setWireframeMode(&Renderer::setWireframeRendering, &renderer, std::placeholders::_1);
setWireframeMode(true);
Now, whenever we call setWireframeMode
, we are internally calling renderer.setWireframeRendering
. When I first saw this, it blew my mind. It’s probably second nature to anyone familiar with functional languages, but for someone rigidly set in a C++ mindset, it was a breath of fresh air.
However, you might be thinking about how slow std::function
might be. Remember we are just implementing a console; the only time we’re going to execute one of these std::function
s is when the user brings up the console and executes a command, so the overhead of std::function
is not noticeable.
Hopefully I’ve convinced you that std::function
is awesome, but we still have a bit of a problem…
Parsing the command line
Let’s take another look at our interface definition:
Class CallbackMap {
public:
void storeCallback(const std::string& functionName, Callback function);
void call(const std::string& functionName, const std::string& args);
private:
std::unordered_map<std::string, Callback> callbackMap;
}
Callback
is just one type. We can’t store std::function
objects taking arbitrary arguments in callbackMap
. For example, what if we wanted to store both add
, which requires a std::function<void(int, int)>
and setWireframeRendering
, which requires std::function<void(bool)>
? Those are two distinct types.
Well, we’d really like to just abstract callback arguments down to space-delimited lists. That is, add
and setWireframeRendering
could both just take a std::string
, and parse out the arguments they expect. For example, our setWireframeRendering
method would look like this:
Renderer::setWireframeRendering(const std::string& args) {
this->wireframe = (args == "true");
}
Now, all callbacks are just one type! But… gosh, that’s gonna be painful. We basically need to write a wrapper function for every callback we now want to implement.
To be honest, I never did come up with something ideal, but here’s my solution. Instead of storing the actual callback inside callbackMap
, I store a lambda which does the argument parsing for you. If you are not familiar with lambdas, they are essentially functions defined at runtime.
Callback
is defined as std::function<void(std::string)>
, but it’s not the “real” type of our callbacks. There’s one intermediate step to translate our actual callback functions into something we can store in callbackMap
. For that intermediate step, I wrote several helper functions which all look like this:
template <class T1>
Callback defineCallback(std::function<void(T1)> func)
{
auto f = [func](const std::string& args) {
T1 v1;
std::stringstream sstream;
sstream << args;
sstream >> v1;
// There should be some additional error handling here
func(v1);
};
return f;
}
This method defines a lambda which stores the real callback, func
, and takes a single argument, args
. When the lambda is executed, it parses the argument using a std::stringstream
and passes the now-typed argument to func
. Here’s defineCallback
in action:
CallbackMap callbackMap;
callbackMap.storeCallback(defineCallback<int, int>(&add));
callbackMap.storeCallback(defineCallback<bool>(std::bind(&Renderer::setWireframeRendering, &renderer, std::placeholders::_1)));
There’s a couple of downsides with this method.
- We’re relying on
std::stringstream
to do our parsing, and it doesn’t always do what we want. For example, it won’t parsewireframe true
correctly, because “true” is not parsed as a boolean.wireframe 1
does work, though so doeswireframe 1.5
, orwireframe -1
. - If a function takes more than one argument (or no arguments),
defineCallback
must be overridden appropriately to take more template arguments. For example, the two-argument version is very similar to the first:
template <class T1, class T2>
Callback CallbackMap::defineCallback(std::function<void(T1,T2)> func)
{
auto f = [func](const std::string& args) {
T1 v1;
T2 v2;
std::stringstream sstream;
sstream << args;
sstream >> v1 >> v2;
func(v1,v2);
};
return f;
}
In practice, I only defined the 0 to 3 argument versions of defineCallback
, and that was enough.
Final Touches
Now that we have a method to pass the callbacks in, we can finally get around to storing and calling them. Most of the complexity has been hidden away in defineCallback
and std::bind
, and now our CallbackMap
methods are very simple.
void CallbackMap::storeCallback(const std::string& callbackName, std::function<void(std::string)> callback)
{
callbackMap[callbackName] = callback;
}
bool CallbackMap::call(const std::string& callbackName, const std::string& args)
{
auto iter = map.find(callbackName);
if (iter == map.end()) {
// Callback not found
return false;
}
iter->second(args);
return true;
}
So, our final approach is summarized like this:
- The real callback,
f
, is passed todefineCallback
, which generates a lambda. - The lambda is stored in
callbackMap
with an associated stringfunctionName
. - Later,
callbackMap.call(functionName, args)
is executed. This pulls the lambda out and executes it. - The lambda parses the space-delimited arguments in
args
using astd::stringstream
. - The callback,
f
, is called with the parsed arguments.
Confused, or TL;DR?
I have the source code for the CallbackMap
in a gist, which you can find here. It includes additional error handling which I didn’t go over in this post. You can also take a look at the classes and some usage in the actual repo for my project SpiderGame.