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. The following program will log out a memory address:
#include <iostream>
bool isEven(int Number) {
return Number % 2 == 0;
}
int main() { std::cout << &isEven; }
00007FF6734B1947
Like with any other type of pointer, this memory address can be stored as a variable and passed around our application as needed. Below, we show two examples of this:
bool isEven(int Number) {
return Number % 2 == 0;
}
void SomeFunction(auto Function) {
// ...
}
int main() {
auto isEvenPtr{&isEven};
SomeFunction(&isEven);
}
Above, we used auto
to have the compiler figure out our data type, but it’s worth considering what data type function pointers 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 this lesson, we’ll use the native approach.
We can assign a value to a function pointer in the normal ways:
bool isEven(int x){
return x % 0 == 2;
};
bool isOdd(int x){
return !isEven(x);
};
int main() {
// Initialisation
bool (*FunctionPtr)(int){&isEven};
// Updating
FunctionPtr = &isOdd;
// Function pointers can also be a nullptr
FunctionPtr = nullptr;
}
Below, we have a pointer that stores a function that returns nothing, and accepts no arguments:
void (*FunctionPtr)();
In this example, we have a pointer to function that returns an int
, and accepts two int
arguments:
int (*FunctionPtr)(int, int);
Below, we have a function that returns nothing, and accepts two arguments: a Player
pointer, and a constant Player
reference
void (*Example3)(Player*, const Player&);
const
Function PointersFunction pointers can also be marked as const
, preventing that pointer from being updated:
void SomeFunction(){};
void AnotherFunction(){};
int main() {
void (*const ConstExample)(){&SomeFunction};
ConstExample = &AnotherFunction;
}
error: 'ConstExample': you cannot assign to a variable that is const
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. Imagine we have the following code:
void (*FuncA)(Player*, const Player&) {
&MyFunction
};
void (*FuncB)(Player*, const Player&) {
&MyFunction
};
void (*FuncC)(Player*, const Player&) {
&MyFunction
};
void SomeFunction(
void (*F)(Player*, const Player&),
int SomeInt){
// Code
}
This can be made much more readable by adding a using
statement to alias our verbose void(*)(Player*, const Player&)
type to something friendlier, like PlayerHandler
:
using PlayerHandler =
void(*)(Player*, const Player&);
PlayerHandler FuncA{&MyFunction};
PlayerHandler FuncB{&MyFunction};
PlayerHandler FuncC{&MyFunction};
void SomeFunction(PlayerHandler F, 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.
#include <iostream>
bool isEven(int Number) {
return Number % 2 == 0;
}
int main() {
auto isEvenPtr{&isEven};
bool is4Even{(*isEvenPtr)(4)};
if (is4Even) std::cout << "4 is even";
}
4 is even
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>
bool isEven(int Number) {
return Number % 2 == 0;
}
int main() {
auto isEvenPtr{&isEven};
if (isEvenPtr) {
std::cout << "isEvenPtr is available";
std::cout << "\n4 is "
<< ((*isEvenPtr)(4) ? "even" : "odd");
}
bool (*isOddPtr)(int){nullptr};
if (!isOddPtr) {
std::cout << "\nisOddPtr is a nullptr";
}
}
isEvenPtr is available
4 is even
isOddPtr is a nullptr
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 Player
in that party met some requirements.
This program is very similar to what we created in the previous lesson. The main difference is we’ve now updated our all_of()
function to specify that it explicitly requires a function pointer, whereas previously we were using a template function.
The type of function pointer it accepts has been aliased to Handler
:
#include <iostream>
class Player {/*...*/};
class Party {
public:
using Handler = bool (*)(const Player&);
bool all_of(Handler Predicate) {
return Predicate(PlayerOne) &&
Predicate(PlayerTwo) &&
Predicate(PlayerThree);
}
private:
};
bool PlayerIsAlive(const Player& P) {
return P.isAlive();
}
bool PlayerIsOnline(const Player& P) {
return P.isOnline();
}
bool PlayerIsAtLeastLevel50(const Player& P) {
return P.GetLevel() >= 50;
}
int main() {
Party MyParty;
if (MyParty.all_of(PlayerIsAlive)) {
std::cout << "Everyone is alive";
}
if (MyParty.all_of(PlayerIsOnline)) {
std::cout << "\nEveryone is online";
}
if (!MyParty.all_of(PlayerIsAtLeastLevel50)) {
std::cout << "\nNot everyone is level 50+";
}
}
Everyone is alive
Everyone is online
Not everyone is level 50+
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 all_of()
function, we don’t 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.
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. External code can query our Party
class and the Player
objects it manages in various ways, but the Party
class retains control over how those queries are handled.
If we want to change how our party works by, for example, expanding the size of the party, or changing how the underlying Player
objects are stored, we only need to modify our Party
class and the all_of()
function within it.
all_of()
, any_of()
, and for_each()
Above, we only implemented an all_of()
function, but in real scenarios, we often need a few more. For example, our class becomes more useful if it offers a few different variations of this idea:
ell_of()
function, which we implemented above, returns true
if every object we’re managing satisfies a provided predicate. A function that implements this is sometimes alternatively called all()
or every()
any_of()
function, which returns true
if any object we’re managing satisfies a provided predicate. A common alternative name for this function is any()
or some()
for_each()
function, which simply provides every object we’re managing to a function, for that function to implement whatever logic is required.We’ve updated our example Party
class with all of these methods below:
#include <iostream>
class Player {/*...*/};
class Party {
public:
using PlayerPred = bool (*)(const Player&);
bool all_of(PlayerPred Predicate) {
return Predicate(PlayerOne) &&
Predicate(PlayerTwo) &&
Predicate(PlayerThree);
}
bool any_of(PlayerPred Predicate) {
return Predicate(PlayerOne) ||
Predicate(PlayerTwo) ||
Predicate(PlayerThree);
}
using PlayerHandler = void (*)(const Player&);
void for_each(PlayerHandler Function) {
Function(PlayerOne);
Function(PlayerTwo);
Function(PlayerThree);
}
private:
};
bool PlayerIsOnline(const Player& P) {
return P.isOnline();
}
bool PlayerIsHealer(const Player& P) {
return P.isHealer();
}
void ReadyCheck(const Player& P) {
std::cout << P.GetName() << " is Ready!\n";
}
int main() {
Party MyParty;
if (MyParty.all_of(PlayerIsOnline)) {
std::cout << "Everyone is Online\n";
}
if (MyParty.any_of(PlayerIsHealer)) {
std::cout << "Someone is a Healer\n";
}
MyParty.for_each(ReadyCheck);
}
Everyone is Online
Someone is a Healer
Roderick is Ready!
Anna is Ready!
Robert is Ready!
The previous pattern where a custom type like our Party
container needs to give external code the ability to iterate over a collection it is managing is very common.
As such, there are standardized approaches for implementing this pattern. These approaches are iterators and ranges.
If we implement support for iterators and ranges for our container, we then also gain access to standardized algorithms. This includes algorithms that cover the use cases we showed above.
For example, the standard library includes algorithms called std::all_of()
, std::any_of()
and std::for_each()
These algorithms work with any containers that provide iterators and ranges so, if we add support for those concepts to our custom type, it immediately becomes compatible with these functions. As such, we don’t need to create them ourselves.
We cover iterators, ranges, and standard library algorithms in much more detail later in this course.
When using these techniques, a common requirement we’ll have is the ability to provide additional arguments to our functions. In the following example, we check if every Player
is at least level 40
:
#include <iostream>
class Player {/*...*/};
class Party {/*...*/};
bool MinLevel40(const Player& P) {
return P.GetLevel() >= 40;
}
int main() {
Party MyParty;
if (MyParty.all_of(MinLevel40)) {
std::cout << "Everyone is level 40+";
}
}
Everyone is level 40+
Fixing the level check to 40
like this is not particularly flexible - we’ll want to be able to change that value, so it should be a parameter. But, when dealing with first-class functions, it might not be especially obvious how to do that.
If the values we want to check are known at compile time, we could solve this using a function template:
#include <iostream>
class Player {/*...*/};
class Party {/*...*/};
template <int Min>
bool MinLevel(const Player& P) {
return P.GetLevel() >= Min;
}
int main() {
Party MyParty;
if (MyParty.all_of(MinLevel<40>)) {
std::cout << "Everyone is level 40+";
}
if (!MyParty.all_of(MinLevel<50>)) {
std::cout << " but not level 50+";
}
}
Everyone is level 40+ but not level 50+
If these values are not known at compile time, there are a few other ways we could solve this problem. We’ll cover them throughout this chapter, starting with functors in the next lesson.
In this lesson, we've explored how function pointers enable the storage and passing of functions just like any other data type. The main points we learned include:
auto
can simplify the declaration of function pointers when the compiler can deduce the type, and we’ll introduce friendler ways to declare functional types later in the chapter.nullptr
, and checking for this before dereferencing is a good practice.Learn about function pointers: what they are, how to declare them, and their use in making our code more flexible
Comprehensive course covering advanced concepts, and how to use them on large-scale projects.