Function Pointers
Learn about function pointers: what they are, how to declare them, and their use in making our code more flexible
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);
}
Function Pointer Types
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;
}
More examples
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&);
Using const
with Function Pointers
Function 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
Type Aliases with Function Pointers
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
}
Calling Functions through a Pointer
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
Implicit Referencing and Dereferencing
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);
A Complete Example
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+
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.
The all_of()
, any_of()
, and for_each()
Algorithms
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:
- An
ell_of()
function, which we implemented above, returnstrue
if every object we're managing satisfies a provided predicate. A function that implements this is sometimes alternatively calledall()
orevery()
- An
any_of()
function, which returnstrue
if any object we're managing satisfies a provided predicate. A common alternative name for this function isany()
orsome()
- A
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!
Using Template Functions
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.
Summary
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:
- Function pointers allow functions to be stored in variables and passed around within a program.
- The syntax for declaring function pointers includes the return type, parameter list, and an asterisk (*) to denote a pointer.
- Using
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. - Function pointers can be passed as arguments to functions, enabling callback mechanisms.
- Function pointers can be
nullptr
, and checking for this before dereferencing is a good practice. - Implicit referencing and dereferencing are available with function pointers, allowing for cleaner syntax in certain cases.
- Template functions can be used in conjunction with function pointers to provide additional flexibility, such as passing extra arguments to the pointed-to functions.
Function Objects (Functors)
This lesson introduces function objects, or functors. This concept allows us to create objects that can be used as functions, including state management and parameter handling.