Function Pointers

Learn about function pointers: what they are, how to declare them, and their use in making our code more flexible
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

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

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&);

const 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+

Predicates

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:

  • An 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()
  • An 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()
  • 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!

Preview: Iterators, Ranges, and Standard Library Algorithms

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.

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.

Was this lesson useful?

Next Lesson

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.
Abstract art representing computer programming
Ryan McCombe
Ryan McCombe
Updated
A computer programmer
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, Unlimited Access
A computer programmer
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, unlimited access

This course includes:

  • 125 Lessons
  • 550+ Code Samples
  • 96% Positive Reviews
  • Regularly Updated
  • Help and FAQ
Next Lesson

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.
Abstract art representing computer programming
Contact|Privacy Policy|Terms of Use
Copyright © 2024 - All Rights Reserved