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".

For Context
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';
      |                  ^~~~~~~~~
Tip
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.

Key Concept
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.

Best Practice
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.