Recently I had an interesting task on my work - a cheat system for a game. All the system should do is to call functions during apllication run with console commands (game console). Functions can accept different number of arguments with different types.
In other words the system should be able to call a function:
bool foo(int, float);
with a console command:
foo 42 10.5
I’ll not describe how I’m receiving this string as well how I’m parsing it. Let’s assume that we’re operating with a vector of strings where the first element - the name of the function and the rest - function parameters. So at some point er have this vector:
{ "foo", "42", "10.5" }
And we need somehow call a correct function and pass a correct number of parameters with a correct type.
There’s no way to call an unknown function just knowing it’s name in C++. At least I don’t know such a way. So in order to call something we first need register it. By registration I mean storing of a function pointer together with it’s name. The std::map
is a good candidate for that. But I don’t store raw pointer. Instead, I have a wrapper class, called Cheat
, for my function. Cheat is a templated class with function arguments types as template parameters. It’s declaration looks like this:
template<typename ...Args>
class Cheat : public CheatBase
{
Cheat(void(* const funcPtr)(Args...)) : func{ funcPtr }
{
}
// ...
private:
void(* const func)(Args...);
}
Here func
is our pointer to function which we need to call later.
For simplcity the only functions that can be passed are global or static functions or lambdas without capture (such a lambda can be casted to function pointer). But member functions can be added with additional template argument and a little bit of code.
As you already guessed, it’s not possible to store instances of this class directly in std::map
because for every new function with a new signature the Cheat
class type will be different. For example Cheat<int>
for the function taking one int
parameter and Cheat<int, float>
for the function takind an int
and a float
. Therefore they all should have common base class. It’s very simple:
class CheatBase
{
public:
virtual ~CheatBase() {};
virtual bool call(const std::vector<std::string>& cheatParams) = 0;
};
It’s an abstract class with pure virtual function which takes an array of parameters as strings which will be converted to corresponding types.
Instead of
call()
function we could useoperator()
function.
Now we can create cheats like this:
CheatBase* cheat{new Cheat<int, float>};
Or with a helper:
template<typename ...Args>
std::shared_ptr<CheatBase> makeCheat(void(*funcPtr)(Args... arguments))
{
return std::make_shared<Cheat<Args...>>(funcPtr);
}
myMap["foo"] = makeCheat(&foo);
Thanks to type deduction we don’t need to type template parameters for the function.
Here for simplcity I’m passing arguments by value. The better approach is to use perfect forwarding.
Btw, the strange looking template<typename ...Args>
is called a parameter pack. And together with templated class it’s called variadic template. This is one of the amazing features of modern C++. I highly recommend you to investigate this topic. It’s really really cool! I can recommend for the start this great posts - one, two.
That was pretty standard and boring. And now the interesting part - the actual cheat implementation!
The heart of the Cheat
class is a call()
function override:
bool call(const std::vector<std::string>& cheatParams) override
{
if (sizeof...(Args) != cheatParams.size()) // # <1>
{
// if a number of passed arguments is not equal to a number of parameters declared in cheat return false - this is an error.
return false;
}
callHelper(cheatParams, std::index_sequence_for<Args...>{});
return true;
}
<1> sizeof...(Args)
return a number of arguments of the parameter pack that was used during Cheat
class instantiation.
First we check that we passed correct number of arguments. In the number is wrong simply do nothing and return. And all the magic happens in callHelper()
function. Don’t think about index_sequence_for<Args...>
for now, we’ll come to this later:
template <std::size_t... Idx>
void callHelper(const std::vector<std::string>& strArgs, std::index_sequence<Idx...>)
{
(func)(fromString<Args>(getStringFromArray<Idx>(strArgs))...);
}
Wow! Looks scary. Actually, not. callHelper()
is a variadic templated function which accepts a vector with string arguments (and which should be converted to appropriate types) and a sequence of integers. What are this integers? Why? I can better explain it if we’ll start from the end. Let’s stick to foo(int, type)
function signature for the rest of this post.
In the very very end I need to call this function with correct parameters, for example:
foo(42, 10.5);
But I have only a vector of strings {"42", "10.5"}
. I need some conversion function that will return a correct type from the corresponding string. Moreover I need to call this function several times - once for each parameter. Let’s call this conversion function fromString
:
foo(fromString("42"), fromString("10.5"));
In order to convert to correct type we’ll use templated function with overload for every type I need. For the case with int
the overload is (for the float
it’s similar, just replace int
to float
and std::stoi()
to std::stof()
):
template<typename T>
typename std::enable_if<std::is_same<T, int>::value, int>::type fromString(const std::string& str)
{
return std::stoi(str);
}
The SFINAE technique is used here. Good explanation of what it is can be found in this awesome blog. In short - std::is_same<T, int>::value
will return true
if T
is int
and false
overwise. Next, std::enable_if<true, int>::type
will return int
and std::enable_if<false, int>::type
simply will not compile. After substitution we’ll have:
template<int>
int fromString(const std::string& str)
{
return std::stoi(str);
}
And additional feature of it is that we’ll get a compile time error if we’ll use a type for which there’s no overload exist! Awesome, this type of error is much much better than an exception during runtime.
Having all this we can create a first version of our callHelper()
function:
void callHelper(const std::vector<std::string>& strArgs)
{
foo(fromString<Args>(getStringFromArray(strArgs))...);
}
As you remember, Args
is a parameter pack. And fromString<Args>()...
is a parameter pack expansion. There’re strict rules how parameter pack is expanded. For our case with int
and float
it will be expanded to:
void callHelper(const std::vector<std::string>& strArgs)
{
foo(fromString<int>(getStringFromArray(strArgs)), fromString<float>(getStringFromArray(strArgs)));
}
We already have two fromString()
overloads for our types. Now the trick is to pass the correct string from the vector to them, i.e. implement getStringFromArray()
function. The naive approach would be to remove getStringFromArray()
completely and just use strArgs
vector together with some counter which will be incremented every time we access a vector element:
void callHelper(const std::vector<std::string>& strArgs)
{
size_t counter{0};
foo(fromString<int>(strArgs[counter++], fromString<float>(strArgs[counter++]));
}
Unfortunately this will not work. The C++ standard does not specify the order of function arguments eveluation. That means it can differ from compiler to compiler. And it’s absolutelly possible to have this setup (remember - we have { "42", "10.5" }
in our vector):
foo(fromString<int>(strArgs[1]), fromString<float>(strArgs[0])); // notice how we're passing wrong arguments
We need instead a robust solution that will work across compilers. Let’s rewrite callHelper
slightly:
void callHelper(const std::vector<std::string>& strArgs)
{
foo(fromString<int>(getStringFromArray<0>(strArgs)), fromString<float>(getStringFromArray<1>(strArgs)));
}
Notice the extra template parameters <0>
and <1>
. And here the definition of getStringFromArray()
function:
template <std::size_t N>
std::string getStringFromArray(const std::vector<std::string>& strArgs)
{
return strArgs[N];
}
Now no matter what is the evaluation order our function will return correct string, since <0>
and <1>
are template parameters and will always be in right sequence. And this order is guaranteed by sequence of integers that we will use. For the moment let’s not think how we create one but see what happens when the function receives it:
template <std::size_t... Idx>
void callHelper(const std::vector<std::string>& strArgs, std::index_sequence<Idx...>)
{
foo(fromString<Args>(getStringFromArray<Idx>(strArgs))...);
}
The function itself templated with non-type parameter pack, in simple words the template parameters are integers and their number is equal to the number of Args
(Cheat
class parameter pack). This Idx
sequence will be deduced from the function’s second unnamed argument std::index_sequence<Idx...>
. Do you see, we even don’t have the name for it, because we don’t use this parameter in the function’s body! The sole reason for this argument is to provide compile-time integers ...Idx
. In the function’s body the two parameter packs - Args
and Idx
will be expanded together simultaneously according to aforementioned rules. In our case everything will be expanded to:
template <0, 1> // this is deduced from the function's second argument
void callHelper(const std::vector<std::string>& strArgs, std::index_sequence<0, 1>)
{
foo(fromString<int>(getStringFromArray<0>(strArgs)), fromString<float>(getStringFromArray<1>(strArgs)));
}
Amazing, isn’t it?
There’s a last piece of puzzle left - how to get this integer sequence? The bad news - in C++11 this should be done manually. Here the great explanation how to do this. And actually on my work I have to use this solution.
But the happy owners of C++14 compliant compiler (and me in this post) can use standard sequence of integers. I’ll put here explanation directly from the link:
A helper alias template std::index_sequence_for is defined to convert any type parameter pack into an index sequence of the same length.
In other words, <...Args>
which, for example, can be <int, float, std::string, double>
will be converted to std::index_sequence<0, 1, 2, 3>
.
Putting it all together we can call our helper like this:
callHelper(cheatParams, std::index_sequence_for<Args...>{});
Where, again, Args...
is a Cheat
class parameter pack.
The source code together with usage example can be found here.