Understanding Function Pointers
Store and invoke functions through pointer variables.
What Are Function Pointers?
A function pointer holds the address of a function instead of the address of an object. Once you have one, the same call site can invoke different functions depending on what the pointer currently holds, which is what makes callbacks and pluggable behavior possible.
The idea rests on something you may not have needed to think about yet: a function occupies memory, at an address, exactly as an object does. Calling it jumps execution to that address. A function also has a type, made up of its return type and its parameter types, so int calculate() has the type "function taking nothing and returning int".
Function pointers are an advanced topic. If you are working through the basics, skimming this lesson is fine. The parts worth taking away are what a callback is and why
std::function is usually the better spelling.
The Mistake That Reveals Them
Most people meet function pointers by accident, by forgetting the parentheses on a call:
#include <iostream>
int calculate()
{
return 42;
}
int main()
{
std::cout << calculate << '\n';
return 0;
}
Output:
1
Not 42. Naming a function without parentheses does not call it; the name converts to a function pointer holding its address. operator<< has no overload for printing a function pointer, so the pointer converts to bool instead, and since a function always has a real address, that bool is true. GCC will tell you as much while compiling:
s.cpp:10:18: warning: the address of 'int calculate()' will never be NULL [-Waddress]
10 | std::cout << calculate << '\n';
| ^~~~~~~~~
Some compilers print the address instead as an extension. To ask for the address portably enough to be useful, convert to a void pointer with
reinterpret_cast<void*>(calculate), which is implementation-defined but works on most platforms.
Declaring a Function Pointer
The declaration syntax is genuinely awkward:
int (*action)();
That is a pointer named action to a function taking no parameters and returning int. It can point at any function of that exact type.
The parentheses around *action are not decoration. Without them, int* action() declares a function named action returning int*, which is a completely different thing.
const placement follows the same logic as it does for object pointers, just shifted:
int (*const action)(); // const pointer: cannot be repointed
Putting const before int would instead describe a function returning a const int.
Pointing It Somewhere and Calling It
A function pointer can be initialized or assigned from a function name, with or without &, and can hold nullptr like any other pointer. Calling through it works with or without an explicit dereference:
#include <iostream>
int calculate()
{
return 42;
}
int process()
{
return 84;
}
int main()
{
int (*action)(){ &calculate };
std::cout << "Implicit dereference: " << action() << '\n';
std::cout << "Explicit dereference: " << (*action)() << '\n';
action = process;
std::cout << "After repointing: " << action() << '\n';
action = nullptr;
if (action)
std::cout << "Never reached\n";
else
std::cout << "Pointer is null, not calling it\n";
return 0;
}
Output:
Implicit dereference: 42
Explicit dereference: 42
After repointing: 84
Pointer is null, not calling it
Both call forms are accepted by modern compilers, and the implicit one reads better. The nullptr check matters: calling through a null function pointer is undefined behavior, so validate any pointer that might not have been set.
Watch the difference between assigning the function and assigning its result. action = process; stores the address. action = process(); calls process and tries to store the returned int, which will not compile as a function pointer assignment.
Default Arguments Do Not Come Along
When the compiler sees an ordinary call to a function with default arguments, it fills them in during compilation. A call through a function pointer is resolved at run time, so there is nothing in place to do that rewriting.
Default arguments are applied at compile time, so they are not applied when a function is called through a function pointer.
There is a use for this. Because the pointer's type names the exact signature, it can pick one overload out of a set that a plain call would find ambiguous:
#include <iostream>
void display(int)
{
std::cout << "display(int)\n";
}
void display(int, int = 25)
{
std::cout << "display(int, int)\n";
}
int main()
{
using DisplayPtr = void(*)(int);
DisplayPtr fn{ display };
fn(10);
static_cast<void(*)(int)>(display)(10);
return 0;
}
Output:
display(int)
display(int)
Both forms select the single-parameter overload, because that is the type being asked for.
Callbacks
The main reason to use function pointers is to hand a function to another function. A function used this way is a callback function.
Sorting is the standard illustration. A comparison sort walks the data, compares pairs, and rearranges based on the result. Only the comparison decides the ordering, so if the caller supplies that piece, one sort routine can order data any way the caller likes:
#include <iostream>
#include <utility>
bool putSmallestFirst(int x, int y)
{
return x > y;
}
bool putLargestFirst(int x, int y)
{
return x < y;
}
void sortInPlace(int* collection, int size, bool (*compare)(int, int))
{
if (!collection || !compare)
return;
for (int current{ 0 }; current < size - 1; ++current)
{
int best{ current };
for (int scan{ current + 1 }; scan < size; ++scan)
{
if (compare(collection[best], collection[scan]))
best = scan;
}
std::swap(collection[current], collection[best]);
}
}
void print(const int* collection, int size)
{
for (int i{ 0 }; i < size; ++i)
std::cout << collection[i] << ' ';
std::cout << '\n';
}
int main()
{
int scores[]{ 30, 50, 20, 10, 40 };
constexpr int size{ static_cast<int>(std::size(scores)) };
sortInPlace(scores, size, putSmallestFirst);
print(scores, size);
sortInPlace(scores, size, putLargestFirst);
print(scores, size);
return 0;
}
Output:
10 20 30 40 50
50 40 30 20 10
The sort itself never changed. Only the function passed in did.
When you design an interface like this, shipping a few ready-made callbacks is a kindness, so callers do not each rewrite putSmallestFirst. You can even make one the default: void sortInPlace(int* collection, int size, bool (*compare)(int, int) = putSmallestFirst);, as long as putSmallestFirst is declared before that point.
Three Ways to Make It Readable
Raw function pointer syntax gets unpleasant fast, especially in a parameter list. Three tools help:
| Approach | Declaration | Trade-off |
|---|---|---|
| Type alias | using CompareFunction = bool(*)(int, int); |
Names the type once, reads like a normal parameter |
std::function |
std::function<bool(int, int)> |
Clearest syntax, and accepts lambdas and other callables |
auto |
auto action{ &calculate }; |
Shortest, but hides the parameter and return types |
std::function lives in <functional> and puts the return type and parameters in a signature-shaped form. It only supports the implicit call syntax, not explicit dereference:
#include <functional>
#include <iostream>
int calculate(int x)
{
return x * 2;
}
int process(int x)
{
return x * 3;
}
int main()
{
using Transform = std::function<int(int)>;
Transform action{ &calculate };
std::cout << "Via std::function: " << action(15) << '\n';
action = &process;
std::cout << "After reassignment: " << action(15) << '\n';
auto deduced{ &calculate };
std::cout << "Via auto: " << deduced(15) << '\n';
return 0;
}
Output:
Via std::function: 30
After reassignment: 45
Via auto: 30
Note that a type alias for std::function has to spell out its template arguments, since there is no initializer for the compiler to deduce them from.
Prefer
std::function to raw function pointer syntax. Use it directly where a callable type appears once, and give it a type alias where the same type appears repeatedly.
Summary
What they are: variables holding the address of a function. A function name used without parentheses converts to a function pointer, which is why printing one shows 1 rather than a result.
Declaring: returnType (*name)(parameters), with the parentheses around *name required, and const after the asterisk for a pointer that cannot be repointed.
Assigning: from a function name with or without &, or from nullptr. Assigning process() rather than process stores a return value instead of an address, and will not compile.
Calling: action() or (*action)(), both accepted. Check for null first, because calling through a null function pointer is undefined behavior.
Default arguments: not applied through a function pointer, since they are filled in at compile time and the call resolves at run time. This makes function pointers useful for disambiguating overloads.
Callbacks: functions passed to other functions, letting the caller supply one piece of an algorithm, as with the comparison in a sort.
Readability: type aliases, std::function, and auto all tame the syntax. Prefer std::function, with an alias when the type repeats.
How did you find this lesson?
Your rating helps us improve the content.
Create an account to track your progress and access interactive exercises. Already have one? Sign in.
Understanding Function Pointers - Quiz
Test your understanding of the lesson.
Practice Exercises
Function Pointers Basics
Learn to declare, assign, and call functions through function pointers. Practice passing function pointers as arguments to create flexible callback mechanisms.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!