Callback functions

A better way to deal with these button actions is with callback functions. Callback functions in C/C++ are implemented using pointers to functions. They allow you to pass functions around as if they were variables. This means functions can be passed to other functions, returned from functions, or even stored in a variable and called later. This allows us to decouple a specific function from the module that will call it. It is a C style way to change which function will be called at runtime.

Just as pointers to int can only point at int, and pointers to float can only point at float, a pointer to a function can only point at a function with the same signature. An example would be the function:

int Square(int x) 

This function takes a single int as a parameter and returns an int. This return value and parameter list are the function's signature. So, a pointer to this function would be:

int (*)(int); 

We haven't given the function pointer a name, so it should look like this:

int (*pFunc)(int); 
Note that the parentheses around the variable name pFunc are required, otherwise the compiler will think this is a prototype of a function that returns a pointer to an int.

We can now create a pointer to a specific function and call that function through the variable:

int (*pFunc)(int); 
pFunc = Square;
std::cout << "2 Squared is "<< pFunc(2) << std::endl;

The output for the preceding code is as follows:

Figure 8 1 - Function pointer output

Notice that we didn't need to take the address of the Square function (although that syntax is allowed); this is because in C and C++ the name of the function is already a pointer to that function. That is why we can call pFunc without needing to dereference it. Unfortunately, everything about function pointers is weird until you get used to them. You must work at remembering the syntax since it doesn't work the same as pointers to variables.

By looking at a larger example, we can get familiar with this syntax. Let's write a program with three different ways to fill an array with values and a way to print the array:

//Fills array with random values from 0 to maxVal - 1 
void RandomFill(int* array, int size, int maxVal)
{
for (int i = 0; i < size; ++i)
array[i] = std::rand() % maxVal;
}

//Fills array with value
void ValueFill(int* array, int size, int value)
{
for (int i = 0; i < size; ++i)
array[i] = value;
}

//Fills array with ordered values from 0 - maxVal - 1 repeatedly
void ModFill(int* array, int size, int maxVal)
{
for (int i = 0; i < size; ++i)
array[i] = i % maxVal;
}


//Helper to print array
void PrintArray(const int* array, int size)
{
for (int i = 0; i < size; ++i)
std::cout << array[i] << " ";
std::cout << std::endl;
}

Our goal with this program is to write a function that can fill an array with any fill function, including one that hasn't been written yet. Since we have a common function signature, we can create a function called FillAndPrint that will take a pointer to any function with a matching signature as a parameter. This will allow FillAndPrint to be decoupled from a specific fill function and allow it to be used with functions that do not exist yet. The prototype for FillAndPrint will look like this:

void FillAndPrint(void (*fillFunc)(int*, int, int), int* array, int size, int param); 

This is incredibly ugly and difficult to read. So, let's use a typedef to clean up the code a little. Remember that a typedef allows us to give a different, hopefully more readable, name to our type:

//Defines a function pointer type named FillFUnc 
typedef void(*FillFunc)(int*, int, int);

void FillAndPrint(FillFunc pFunc, int* array, int size, int param)
{
pFunc(array, size, param);
PrintArray(array, size);
}

In main, the user of this code can pick which fill function they want to use or even write a completely new one (if the signature is the same), without changing FillAndPrint:

int main(void) 
{
const int SIZE = 20;
int array[SIZE];
//See the Random number generator
std::srand(static_cast<unsigned>(time(0)));
FillAndPrint(ValueFill, array, 20, 3);
FillAndPrint(RandomFill, array, 10, 5);

return 0;
}

Here is what this code would output to the command line:

Figure 8 2 - Using FillAndPrint in different ways

We could even allow the user to pick the fill at runtime if we included a helper function to select and return the correct fill function:

FillFunc PickFill(int index) 
{
switch (index)
{
case 0:
return RandomFill;
case 1:
return ValueFill;
default:
//We could report an error if the value is outside of the
//range, but instead we just use a default
return ModFill;
}
}

//Our Second main example
int main(void)
{
const int SIZE = 20;
int array[SIZE];
int fillChoice;
int param;

//This doesn't properly explain to the user,
//but it is just an example
std::cout << "Enter a Fill Mode and parameter to use"
<< std::endl;
std::cin >> fillChoice;
std::cin >> param;
//See the Random number generator
std::srand(static_cast<unsigned>(time(0)));
FillAndPrint(PickFill(fillChoice), array, 20, param);

return 0;
}

This is a very simple example, but you can already see how using function pointers allows us to write flexible code. FillAndPrint is completely decoupled from any specific function call. Unfortunately, you can also see two flaws with this system. The functions must have the exact same signature, and the parameters of the function must be passed to the user of the function pointer.

These two problems make function pointers interesting and powerful, but not the best solution for in-game buttons that need to support a wide variety of actions with varying parameter lists. Additionally, we might want to support actions that use C++ member functions. So far, all the examples that we have seen were C style global functions. We will solve these problems in a moment, but first we should look at how we will trigger our button click.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.219.236.70