In this lesson, we'll show how to create and use function pointers. This is one of the ways C++ implements first class functions.
First class functions are functions that can be treated like any other data type. For example, they can be stored in variables and passed around, even as arguments and return values in other functions.
For function pointers, the key point is that we can imagine functions existing in an area of memory, like any other variable.
We can see this using the address of operator &
with the name of one of our functions:
#include <iostream>
using namespace std;
bool isEven(int Number) {
cout << "Calling isEven" << endl;
return Number % 2 == 0;
}
int main() {
cout << &isEven << endl;
}
This will log out a memory address, like 0x001627a0
Like with any other type of pointer, this memory address can be stored as a variable, and passed around our application as needed:
auto isEvenPtr { &isEven };
SomeFunction(isEvenPtr);
Above, we used auto
to have the compiler figure out our data type, but it’s worth considering what data type function pointers actually have.
From our earlier lessons on forward declarations and prototypes, we may already be able to predict that the type of a function.
A function's type is a combination of the function’s return type, as well as the type of all of its parameters. Unfortunately, the way we specify this in C++ is quite cryptic.
To create a variable called isEvenPtr
that stores a pointer to a function that returns a boolean, and accepts a single integer, we do this:
bool (*isEvenPtr)(int);
A later lesson on standard library function helpers will provide a better way of declaring function types. But, for now, we’ll use the native approach.
We can assign a value to a function pointer in the normal ways:
// Initialisation
bool (*isEvenPtr)(int) { &isEven };
// Updating
isEvenPtr = &SomeOtherFunction;
// Function pointers can also be nullptr
isEvenPtr = nullptr;
Here are some more examples of function pointer types:
// A function that returns nothing, and accepts
// no arguments
void (*Example1)();
// A function that returns an int, and accepts
// two int arguments
int (*Example2)(int, int);
// A function that returns nothing, and accepts
// two arguments
// - A Character pointer
// - A constant Character reference
void (*Example3)(Character*, const Character&);
Function pointers can also be marked as const
:
void (*const ConstExample)();
// Error - cannot update a const variable
ConstExample = nullptr;
Like with other types, we can use type aliases with function pointers, via the using
statement. This can help us make our code more readable, particularly if our complicated type is going to be repeated in several places. Sections of code that look like this:
void (*FuncA)(Character*, const Character&) {
&MyFunction
};
void (*FuncB)(Character*, const Character&) {
&MyFunction
};
void (*FuncC)(Character*, const Character&) {
&MyFunction
};
void SomeFunction(
void (*Handler)(Character*, const Character&),
int SomeInt){
// Code
}
Can be made much more readable by adding a using
 statement:
using CharacterHandler =
void(*)(Character*, const Character&);
CharacterHandler FuncA{&MyFunction};
CharacterHandler FuncB{&MyFunction};
CharacterHandler FuncC{&MyFunction};
void SomeFunction(
CharacterHandler Handler, int SomeInt){
// Code
}
As with any other pointer, we can dereference it using the *
operator. This will return a function, which we can call using the ()
operator as normal:
(*isEvenPtr)(4); // returns true
Note the additional set of brackets around *isEvenPtr
. This is because the ()
operator has higher precedence than the *
operator, therefore we need to add brackets to ensure the dereferencing happens first.
As function pointers can be nullptr
, we should generally ensure this isn’t the case before we call it. We can do that using an if
statement, in the same way we handle other pointers:
#include <iostream>
using namespace std;
bool isEven(int Number) {
cout << "Calling isEven" << endl;
return Number % 2 == 0;
}
int main() {
auto isEvenPtr { &isEven };
if (isEvenPtr) {
(*isEvenPtr)(4); // returns true
}
bool (*isOddPtr)(int) { nullptr };
if (isOddPtr) {
(*isOddPtr)(4); // never called
}
}
In these examples, we’ve been adding the address-of operator &
and the dereferencing operator *
in the same places we would do were we dealing with a pointer to any other data type.
In the case of function pointers, this is not strictly necessary. When our variables are storing function pointers, the compiler can implicitly take care of this for us, meaning code like this:
auto isEvenPtr { &isEven };
(*isEvenPtr)(4);
Can be simplified to this:
auto isEvenPtr { isEven };
isEvenPtr(4);
In the previous lesson, we introduced a scenario where we’d want to be able to pass a function to another function in our code. We had a Party
class, and we wanted to be able to determine if every Character
in that party met some requirements:
#include <iostream>
#include <memory>
using std::unique_ptr, std::make_unique,
std::cout;
class Character {
public:
bool isAlive() const{ return true; }
};
class Party {
public:
// TODO: add an "every" function
private:
unique_ptr<Character> PlayerOne{
make_unique<Character>()
};
unique_ptr<Character> PlayerTwo{
make_unique<Character>()
};
unique_ptr<Character> PlayerThree{
make_unique<Character>()
};
unique_ptr<Character> PlayerFour{
make_unique<Character>()
};
};
bool isCharacterAlive(
const Character& Character){
return Character.isAlive();
};
int main(){
Party MyParty;
if (MyParty.every(isCharacterAlive)) {
cout << "Everyone is alive!\n";
}
}
We can now make this work by adding the every
function to our Party
class, and letting it accept a pointer to a function that accepts a const Character&
as an argument, and returns a bool
:
class Party {
public:
using Handler = bool (*)(const Character&);
bool every(Handler Predicate) {
return (
Predicate(*PlayerOne) &&
Predicate(*PlayerTwo) &&
Predicate(*PlayerThree) &&
Predicate(*PlayerFour)
);
}
// The rest of the class is unchanged
};
The use of the name “predicate” here is likely to be confusing. In programming, a predicate is a function that returns true or false. Normally, when we create a function (or a variable to store a function) we’d prefer to give it a descriptive name, like isAlive
.
However, in cases like our every
function, we don’t actually know what the function in the argument is going to do. We just know it’s going to return a boolean, so we fall back to the convention of calling it a predicate.
Typically, a predicate will only have one argument - the object it is testing. If the predicate returns true
for a specific object, the object is often described as “matching the predicate”.
Just like that, our Party
class offers a huge amount of flexibility to anyone who uses it. And, critically, it does so without violating principles like encapsulation. Arbitrary code can ask an infinite number of questions of our Party
class and the Character
objects it manages, but the Party
class maintains full control over that process.
If we want to change how our party works by, for example, expanding the size of the party, or changing how the underlying Character
objects are stored, we only need to modify our Party
class and the every
function within it.
In these lessons, we’re only implementing an every
function, but in real scenarios, we often need a few more. Typically, there are at least 3 access patterns we want to support:
What we’ve already implemented - a function that returns true
if every object matches a predicate. Such functions are normally called every
or all
:
bool isCharacterAlive(
const Character& Character){
return Character.isAlive();
}
// is everyone alive?
MyParty.every(isCharacterAlive);
A function that returns true
if at least one object matches a predicate. Such functions are typically called some
or any
:
bool isCharacterAlive(
const Character& Character){
return Character.isAlive();
}
// is anyone alive?
MyParty.some(isCharacterAlive);
A function that gets called on every object. This is typically called forEach
:
void Revive(Character& Character) {
Character.Revive();
}
// Revive everyone
MyParty.forEach(Revive);
The implementation of these would be very similar to our every
 example.
— Reformatted some code examples to improve usability on small screens
— First Published
Comprehensive course covering advanced concepts, and how to use them on large-scale projects.