Callbacks and Function Pointers

Learn to create flexible and modular code with function pointers
This lesson is part of the course:

Game Dev with SDL2

Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games

Free, Unlimited Access
Abstract art representing computer programming
Ryan McCombe
Ryan McCombe
Posted

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.

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:

  • We can use the address-of operator & to create a function pointer
  • We can use the dereferencing operator * to call a function through its pointer

For 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

Callbacks

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

Function Composition

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.

  • The count_if() function can test for any condition, not just whether something is even
  • The isEven() function can be used in any context, not just counting how many even numbers are in a collection

Let’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.

Function Pointer Types

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();
  }
}

Asynchronous Callbacks

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.

Returning Functions

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.

Summary

This lesson covered the basics of first-class functions in C++.

  • We examined how to use, pass, and store function pointers.
  • Practical examples included callbacks, composition, and asynchronous patterns.
  • std::function was introduced as a more readable alternative to raw function pointers.

These techniques enable more flexible and modular code design.

Was this lesson useful?

Next Lesson

Member Function Pointers and Binding

Explore advanced techniques for working with class member functions
Abstract art representing computer programming
Ryan McCombe
Ryan McCombe
Posted
Lesson Contents

Callbacks and Function Pointers

Learn to create flexible and modular code with function pointers

sdl2-promo.jpg
This lesson is part of the course:

Game Dev with SDL2

Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games

Free, Unlimited Access
Ticks, Timers and Callbacks
  • 51.GPUs and Rasterization
  • 52.SDL Renderers
sdl2-promo.jpg
This lesson is part of the course:

Game Dev with SDL2

Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games

Free, unlimited access

This course includes:

  • 53 Lessons
  • 100+ Code Samples
  • 91% Positive Reviews
  • Regularly Updated
  • Help and FAQ
Next Lesson

Member Function Pointers and Binding

Explore advanced techniques for working with class member functions
Abstract art representing computer programming
Contact|Privacy Policy|Terms of Use
Copyright © 2024 - All Rights Reserved