At this point, we’re familiar with the many ways we store and transfer objects around our program. We can pass them as arguments to functions, return them from functions, and store them as variables.
// Passing an int to a function
SetNumber(42);
// Returning an int from a function
int GetNumber() {
return 42;
}
// Storing an int to a member variable
SomeObject.SomeMember = 42;
What might be less familiar is that we can do all of these things with functions, too:
void SomeFunction() {
// ...
];
// Passing a function to a function
SetFunction(MyFunction);
// Returning a function from a function
auto GetFunction() {
return MyFunction;
}
// Storing a function as a member variable
SomeObject.SomeFunction = MyFunction;
This capability gives us a huge amount of flexibility in how we design our program and when used well, it helps us keep our code simple even as our behaviors get more and more complex.
A language that allows functions to be treated like any other data type is said to support first-class functions. C++ supports this concept in a few different ways. In the rest of this chapter, we’ll focus on one of them - function pointers.
In this lesson, we’ll focus on pointers to free functions, that is, functions that are not a member of a class or struct.
// A free function
void SomeFunction() {/*...*/}
class SomeClass {
// A member function
void SomeFunction() {/*...*/}
};
We cover member functions in the next lesson. Creating a pointer to a function uses a similar syntax to any other data type:
&
to create a function pointer*
to call a function through its pointerFor example:
#include <iostream>
void SomeFunction() {
std::cout << "Hello World";
}
int main() {
auto FunctionPtr{&SomeFunction};
(*FunctionPtr)();
}
Hello World
However, when working with function pointers, the &
and *
operators are typically unnecessary, and usually omitted in practice:
#include <iostream>
void SomeFunction() {
std::cout << "Hello World";
}
int main() {
auto FunctionPtr{SomeFunction};
FunctionPtr();
}
Hello World
When we use a function pointer, we intend to provide some component in our program with custom behavior that is defined outside of that component.
That other component can then decide if and when to invoke that behavior. Perhaps it invokes it only if some condition is true. Perhaps it invokes it multiple times. If the other component is an object, it can save that pointer in a member variable, and invoke it later.
A function used in this way is sometimes referred to as a callback. Below, we have a Process()
function that receives some number and a callback in the form of a function pointer. The Process()
function will invoke that callback only if the number is even:
#include <iostream>
void Process(int Number, auto EvenCallback) {
std::cout << "\nProcessing " << Number;
if (Number % 2 == 0) {
EvenCallback();
}
}
void LogEven() {
std::cout << " - that number is even";
}
int main() {
Process(4, LogEven);
Process(5, LogEven);
Process(6, LogEven);
}
Processing 4 - that number is even
Processing 5
Processing 6 - that number is even
Being able to compose functions together in this way gives us a succinct and elegant way to create complex behaviors from smaller, reusable parts.
For example, imagine we have a collection of numbers and we want to count how many of them are even. We could write this process as a single function
#include <algorithm>
#include <iostream>
#include <vector>
int CountEven(std::vector<int>& Numbers) {
int Count{0};
for (int& Num : Numbers) {
if (Number % 2 == 0) {
++Count;
}
}
return Count;
}
int main() {
std::vector Numbers{1, 2, 3, 4, 5};
int EvenCount = CountEven(Numbers);
std::cout << "Number of even elements: "
<< EvenCount;
}
Number of even elements: 2
Alternatively, we could implement it as the composition of multiple smaller functions.
The standard library includes the count_if()
algorithm, which counts the number of elements in a collection that match some condition.
We can specify the condition we’re interested in by passing a function pointer to the algorithm. This argument will point to a function that will be invoked for every element in our collection. It will receive that element as an argument, and should return true
if that element is to be included in the count:
#include <algorithm>
#include <iostream>
#include <vector>
bool isEven(int Number) {
return Number % 2 == 0;
}
int main() {
std::vector Numbers{1, 2, 3, 4, 5};
int EvenCount = std::ranges::count_if(
Numbers, isEven);
std::cout << "Number of even elements: "
<< EvenCount;
}
Number of even elements: 2
This compositional approach has many advantages. Most notably, the count_if()
and isEven()
functions are more useful than our original CountEven()
function, as they can be reused to solve different problems.
count_if()
function can test for any condition, not just whether something is evenisEven()
function can be used in any context, not just counting how many even numbers are in a collectionLet’s see another example. Below, we have a std::vector
of custom Monster
objects, and we want to find out how many of them are still alive:
#include <algorithm>
#include <iostream>
#include <vector>
struct Monster {
int Health;
};
bool isAlive(const Monster& Enemy) {
return Enemy.Health > 0;
}
int main() {
std::vector<Monster> Enemies{
{100}, {0}, {250}};
int AliveCount = std::ranges::count_if(
Enemies, isAlive);
std::cout << AliveCount
<< " enemies are still alive";
}
2 enemies are still alive
We’ll see many more examples of the utility of first-class functions through the rest of this course, and practice with using the technique to solve practical problems.
For simplicity, our earlier examples used auto
to let the compiler deduce the type of our function pointers. Generally, we should prefer to be explicit with our types. Unfortunately, the syntax to specify function pointer types isn’t pretty:
void FunctionA(){};
bool FunctionB(int x) { return true; }
bool FunctionC(int x, float y) { return true; }
int main() {
// A function that returns nothing
// and accepts no arguments
void (*PtrA)(){FunctionA};
// A function that returns a bool
// and accepts an int argument
bool (*PtrB)(int){FunctionB};
// A function that returns a bool and
// accepts int and float arguments
bool (*PtrC)(int, float){FunctionC};
}
When we specify a parameter type as a function pointer, we can also make it optional, allowing callers to provide a callback only if they need to.
Below, we update our earlier Process
function to have a default callback of nullptr
, and update our if
check to prevent it from trying to invoke a callback if it wasn’t provided:
#include <iostream>
void Process(
int Number,
void (*EvenCallback)() = nullptr
) {
std::cout << "\nProcessing " << Number;
if (EvenCallback && Number % 2 == 0) {
EvenCallback();
}
}
void LogEven() {
std::cout << " - that number is even";
}
int main() {
Process(4, LogEven);
Process(5, LogEven);
Process(6);
}
Processing 4 - that number is even
Processing 5
Processing 6
std::function
The standard library includes some helpers to make storing and transferring function pointers easier. One of the most useful is std::function
, which has the immediate benefit of making our types easier to understand:
#include <functional>
void FunctionA(){};
bool FunctionB(int x) { return true; }
bool FunctionC(int x, float y) { return true; }
int main() {
// A function that returns nothing
// and accepts no arguments
std::function<void()> PtrA{FunctionA};
// A function that returns a bool
// and accepts an int argument
std::function<bool(int)> PtrB{FunctionB};
// A function that returns a bool and
// accepts int and float arguments
std::function<bool(int, float)> PtrC{FunctionC};
// When we're providing an initial value, the
// compiler can usually infer the return and
// argument types using Class Template Argument
// Deduction (CTAD)
std::function PtrD{FunctionC};
}
Using it in a function parameter looks like this:
void Process(
int Number,
std::function<void()> EvenCallback = nullptr
) {
std::cout << "\nProcessing " << Number;
if (EvenCallback && Number % 2 == 0) {
EvenCallback();
}
}
So far, our examples have passed a function pointer to another function that invokes (or conditionally invokes) the callback during execution. However, function pointers can also be used asynchronously. That is, in scenarios where the execution is delayed or triggered by events.
A common way to implement this is by storing function pointers as member variables in objects. The object can then invoke the function later in response to some event or situation, effectively decoupling the definition of behavior from its execution time.
For example, let's create a Player
class that notifies an external system when the player is defeated:
// Player.h
#pragma once
#include <iostream>
#include <functional>
class Player {
public:
void TakeDamage (int Damage) {
Health -= Damage;
if (DefeatCallback && Health <= 0) {
DefeatCallback();
}
}
int Health;
std::function<void()> DefeatCallback{nullptr};
};
Now, other systems can define arbitrary behaviors, whilst giving the Player
object control over when those behaviors are executed:
#include <iostream>
#include "Player.h"
void OnDefeat() {
std::cout << "Game Over";
}
int main() {
Player PlayerOne{100, OnDefeat};
PlayerOne.TakeDamage(150);
}
Game Over
This is the basic foundation of a powerful software design principle called the observer pattern. We’ll expand on this system and explore its benefits in more detail later in the chapter.
The final tenet of the first class functions concept involves returning functions from other functions. Function pointers in C++ support this in the way we might expect. We simply list the return type in the function prototype, and return
a pointer that matches that type:
#include <functional>
#include <iostream>
void Greet() {
std::cout << "Hello World\n";
}
std::function<void()> GetFunction() {
return Greet;
}
int main() {
auto Func{GetFunction()};
Func();
// Alternatively:
GetFunction()();
}
Hello World
Hello World
While this technique may be less commonly used than the others we've introduced, it does have valuable applications in certain scenarios.
For example, let’s imagine we have a SuggestAction()
function that evaluates a few different actions (functions). It returns a function pointer with its recommended action, and updates a Utility
integer, estimating the usefulness of that action:
// Actions.h
#pragma once
#include <functional>
#include <iostream>
void Heal() {
std::cout << "Healing...\n";
}
void Attack() {
std::cout << "Attacking...\n";
}
void RunAway() {
std::cout << "Running away...\n";
}
bool canHeal() { return false; }
bool canAttack() { return false; }
int GetAttackDamage() { return 100; }
std::function<void()> SuggestAction(int& Utility) {
if (canHeal()) {
Utility = 150;
return Heal;
} else if (canAttack()) {
Utility = GetAttackDamage();
return Attack;
} else {
Utility = 25;
return RunAway;
}
}
Elsewhere in our code, we can ask this function for its recommended action, but only perform that action if it would be sufficiently impactful:
#include <iostream>
#include "Actions.h"
int main() {
int Utility;
auto Action{SuggestAction(Utility)};
if (Utility >= 100) {
Action();
} else {
std::cout << "Utility (" << Utility << ")"
" is too low - taking no action";
// ...
}
}
Utility (25) is too low - taking no action
This is a simplistic example of a utility system, often used to control the behavior of AI agents in games and other contexts. We’ll expand on this more later in the chapter when we work with pointers to functions that are members of a class or struct.
This lesson covered the basics of first-class functions in C++.
std::function
was introduced as a more readable alternative to raw function pointers.These techniques enable more flexible and modular code design.
Learn to create flexible and modular code with function pointers
Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games