C++ Function Pointers: Declaration Syntax, Callbacks, Dispatch Tables, and std::function

Key takeaways

How function pointers are declared, assigned and called, why captureless lambdas convert to them but capturing lambdas do not, how C callbacks carry state through void*, and where std::function or template parameters are the better choice.

What is Function Pointer?

A function pointer is a variable that stores the memory address of a function, allowing you to call functions indirectly. This is particularly useful for implementing callbacks, event handlers, and dynamic function selection at runtime.

In C++, function pointers enable polymorphic behavior without inheritance, making them valuable for C-style APIs, callbacks and dispatch tables. They are not faster than virtual functions: both are an indirect call through an address known only at run time, and both usually prevent inlining. The advantages are elsewhere. A function pointer is a single machine word, needs no class hierarchy or object, has a stable ABI, and is the only kind of callable that C libraries such as qsort, pthread_create, atexit or signal understand.

Under the hood, a function’s name in most expressions decays to a pointer to its first instruction, the same way an array name decays to a pointer to its first element. That is why int (*funcPtr)(int, int) = add; works without an &. The type of the pointer includes the full signature, return type and parameter types, so the compiler checks every assignment and every call against it.

int add(int a, int b) {
    return a + b;
}

int main() {
    // Declare function pointer
    int (*funcPtr)(int, int) = add;
    
    // Call
    int result = funcPtr(3, 4);  // 7
    std::cout << result << std::endl;
}

Basic Syntax

The syntax for function pointers can look intimidating at first, but it follows a logical pattern. The key is reading it “inside-out”: int (*ptr)(int, int) means “ptr is a pointer to a function that takes two ints and returns an int.”

The parentheses around *ptr are crucial - without them, you’d be declaring a function that returns a pointer, not a pointer to a function.

// Declare function pointer
int (*ptr)(int, int);

// Assign function
int add(int a, int b) { return a + b; }
ptr = add;
// Or
ptr = &add;

// Call
int result = ptr(3, 4);
// Or
int result = (*ptr)(3, 4);

(The two result lines are alternatives; declaring result twice in one scope would not compile.) Both spellings of assignment and call are equivalent because of the decay rule: add and &add produce the same pointer, and ptr(3, 4) and (*ptr)(3, 4) make the same call. Most code uses the shorter forms; the explicit & and * are sometimes kept for readability in C-style code.

The type must match exactly. Assigning double half(double) to an int (*)(int, int) is a compile error such as invalid conversion from 'double (*)(double)' to 'int (*)(int, int)'. There is no implicit conversion even between signatures that look compatible, such as void f(int) and void (*)(long), and no automatic handling of default arguments: a pointer to void g(int x = 0) has type void (*)(int), and calling it with no argument does not compile, because default arguments belong to the declaration, not the function type.

Overloaded functions add a wrinkle: &print is ambiguous if print(int) and print(double) both exist. The target type resolves it (void (*p)(int) = print; picks the int overload), and where there is no target type, such as passing print to a template, you need static_cast<void(*)(int)>(print) to choose.


Simplify with typedef/using

Function pointer syntax can become unwieldy with complex signatures. Type aliases make your code more readable and maintainable. The modern using syntax is preferred over typedef because it’s more consistent with template syntax and easier to read.

This approach is essential when working with arrays of function pointers or when passing function pointers as template parameters.

// typedef
typedef int (*Operation)(int, int);

// using (recommended)
using Operation = int(*)(int, int);

int add(int a, int b) { return a + b; }
int multiply(int a, int b) { return a * b; }

int main() {
    Operation op = add;
    std::cout << op(3, 4) << std::endl;  // 7
    
    op = multiply;
    std::cout << op(3, 4) << std::endl;  // 12
}

(Again, the typedef and using lines are alternatives; declaring Operation twice would be a redefinition only if the types differed, so this particular pair happens to compile, but real code picks one.)

The alias pays off most in declarations that would otherwise nest function-pointer syntax. Compare a function that returns a function pointer: without an alias it is int (*getOp(char c))(int, int);, which almost nobody can read at a glance; with the alias it is Operation getOp(char c);. The same goes for a pointer to a function that takes a callback. An alternative some codebases use is an alias for the function type and then adding * where needed: using OpFn = int(int, int); and OpFn* op = add;, which reads naturally and mirrors how std::function<int(int, int)> spells the signature.


Practical Examples

Example 1: Callback Function

Callbacks are one of the most common uses of function pointers. They allow you to customize behavior without modifying the core algorithm. This pattern is fundamental in event-driven programming, sorting algorithms with custom comparators, and plugin architectures.

The forEach function here demonstrates the strategy pattern - the same iteration logic can perform different operations based on the callback provided.

#include <vector>

void forEach(const std::vector<int>& vec, void (*callback)(int)) {
    for (int x : vec) {
        callback(x);
    }
}

void printValue(int x) {
    std::cout << x << " ";
}

void printSquare(int x) {
    std::cout << x * x << " ";
}

int main() {
    std::vector<int> numbers = {1, 2, 3, 4, 5};
    
    forEach(numbers, printValue);   // 1 2 3 4 5
    std::cout << std::endl;
    
    forEach(numbers, printSquare);  // 1 4 9 16 25
    std::cout << std::endl;
}

The limitation of this forEach becomes obvious the moment you want the callback to remember something, for example to sum the values or print with a prefix chosen at run time. A plain function pointer has nowhere to keep that state except a global variable. C APIs solve this with a companion void* userData parameter that is passed back to the callback unchanged:

void forEachCtx(const std::vector<int>& vec, void (*cb)(int, void*), void* ctx) {
    for (int x : vec) cb(x, ctx);
}
int total = 0;
forEachCtx(numbers, [](int x, void* p) { *static_cast<int*>(p) += x; }, &total);

That pattern is everywhere in C (qsort_r, pthread_create, GUI toolkits), and the captureless lambda converts to the function pointer. It is also where bugs hide: the void* throws away the type, so casting it back to the wrong type is undefined behavior the compiler cannot catch, and passing a pointer to a local that is gone by the time an asynchronous callback fires leads to use-after-free. In C++ code that does not need C compatibility, a template parameter (template <typename F> void forEach(const std::vector<int>&, F f)) accepts lambdas with captures and lets the compiler inline the call, which is how std::for_each is written.

Example 2: Calculator

This calculator example shows how function pointers enable runtime function selection. Instead of using if/switch statements, you can pass the operation as a parameter. This makes the code more extensible - adding new operations doesn’t require modifying existing functions.

This pattern is commonly used in mathematical libraries, scripting engines, and anywhere you need to select algorithms dynamically.

using Operation = int(*)(int, int);

int add(int a, int b) { return a + b; }
int subtract(int a, int b) { return a - b; }
int multiply(int a, int b) { return a * b; }
int divide(int a, int b) { return b != 0 ? a / b : 0; }

int calculate(int a, int b, Operation op) {
    return op(a, b);
}

int main() {
    std::cout << calculate(10, 5, add) << std::endl;       // 15
    std::cout << calculate(10, 5, subtract) << std::endl;  // 5
    std::cout << calculate(10, 5, multiply) << std::endl;  // 50
    std::cout << calculate(10, 5, divide) << std::endl;    // 2
}

calculate does not care which operation it runs; the caller decides. Note, however, that divide returns 0 on division by zero, which makes a genuine error indistinguishable from a real result of 0 (for example calculate(0, 5, divide)). Silently returning a sentinel value is one of the easiest ways to make a bug travel far from its cause. With function pointers, the whole table must share one signature, so error reporting has to be part of that signature: return std::optional<int> from every operation, or throw, or pass an error out-parameter. Integer division by zero is undefined behavior in C++ (on x86 it typically kills the process with SIGFPE), so the check itself is necessary; it is only the choice of 0 as the result that is questionable.

Example 3: Function Table

Function tables (jump tables) are an efficient way to eliminate long chains of if-else or switch statements. By mapping string commands to function pointers, you can implement command processors, interpreters, or dispatch systems. The lookup cost depends on the container: an array indexed by an integer opcode is O(1), a std::unordered_map is O(1) on average, and the std::map used below is O(log n) with string comparisons, which is perfectly fine for a handful of commands.

This approach is particularly valuable in embedded systems, game engines, and anywhere performance matters more than the slight complexity overhead.

#include <map>
#include <string>

using Command = void(*)();

void cmdHelp() {
    std::cout << "Help" << std::endl;
}

void cmdQuit() {
    std::cout << "Quit" << std::endl;
}

void cmdStatus() {
    std::cout << "Status check" << std::endl;
}

int main() {
    std::map<std::string, Command> commands = {
        {"help", cmdHelp},
        {"quit", cmdQuit},
        {"status", cmdStatus}
    };
    
    std::string input;
    while (true) {
        std::cout << "> ";
        std::cin >> input;
        
        if (input == "quit") break;
        
        auto it = commands.find(input);
        if (it != commands.end()) {
            it->second();  // Call function
        } else {
            std::cout << "Unknown command" << std::endl;
        }
    }
}

The main benefit of the table is not speed but data-driven dispatch: adding a command is one line in the map, the list of commands can be printed for help automatically, and the same table could be filled from a plug-in registration function. commands.find(input) is the right lookup; commands[input]() would insert a null entry for an unknown command and then call it, which is undefined behavior and usually a crash.

Two details in this loop are worth fixing in real code. cmdQuit is registered but never called, because "quit" is handled before the lookup, so the table and the control flow disagree about what quit does. And std::cin >> input inside while (true) spins forever if input ends (Ctrl+D, or a closed pipe), because the failed read leaves input unchanged; while (std::cin >> input) stops cleanly at end of input. For richer commands, the table’s value type is usually std::function<void(const std::vector<std::string>&)> so each handler receives its arguments and can capture state.

Example 4: Sort Comparison Function

#include <algorithm>
#include <cstdlib>   // std::abs(int)
#include <vector>

bool ascending(int a, int b) {
    return a < b;
}

bool descending(int a, int b) {
    return a > b;
}

bool byAbsoluteValue(int a, int b) {
    return std::abs(a) < std::abs(b);
}

void sortVector(std::vector<int>& vec, bool (*compare)(int, int)) {
    std::sort(vec.begin(), vec.end(), compare);
}

int main() {
    std::vector<int> numbers = {-5, 2, -8, 1, 9, -3};
    
    sortVector(numbers, ascending);
    for (int x : numbers) std::cout << x << " ";
    std::cout << std::endl;  // -8 -5 -3 1 2 9
    
    sortVector(numbers, descending);
    for (int x : numbers) std::cout << x << " ";
    std::cout << std::endl;  // 9 2 1 -3 -5 -8
    
    sortVector(numbers, byAbsoluteValue);
    for (int x : numbers) std::cout << x << " ";
    std::cout << std::endl;  // 1 2 -3 -5 -8 9
}

Comparators passed to std::sort must implement a strict weak ordering: compare(a, a) must be false, and if a comes before b, b must not come before a. ascending and descending satisfy that; a comparator written as return a <= b; does not, and passing it to std::sort is undefined behavior that can, with some inputs and implementations, read past the end of the vector. byAbsoluteValue is valid, but -3 and 3 compare equivalent, so their relative order after std::sort is unspecified; use std::stable_sort if ties must keep their input order.

This example is also the clearest demonstration of the performance difference between function pointers and function objects. std::sort is a template, and when it receives a lambda or std::less<>, each comparison is a direct call the compiler can inline into the sorting loop. When it receives bool (*)(int, int), the comparator’s type is just “some function pointer”, so every comparison is an indirect call unless the optimizer can prove which function it is. That is the same reason std::sort usually beats C’s qsort, which always calls its comparator through a pointer. For a few thousand elements the difference is negligible; for sorting millions of small values, passing a lambda is typically measurably faster.


std::function (C++11)

#include <functional>

int add(int a, int b) {
    return a + b;
}

int main() {
    // Function pointer
    int (*ptr)(int, int) = add;
    
    // std::function (more flexible)
    std::function<int(int, int)> func = add;
    
    // Can also store lambda
    func = [](int a, int b) { return a * b; };
    
    std::cout << func(3, 4) << std::endl;  // 12
}

std::function<int(int, int)> can hold anything callable with that signature, including lambdas that capture variables, which a function pointer cannot. It does this through type erasure: internally it stores the callable in a small buffer (or on the heap if it is too big for the buffer) together with a pointer to code that knows how to call, copy and destroy it. The costs are a larger object (typically 32 bytes versus 8 for a function pointer on 64-bit platforms), a possible allocation when storing large captures, and an indirect call that the compiler rarely inlines. Calling an empty std::function throws std::bad_function_call, whereas calling a null function pointer is undefined behavior.

A reasonable default: accept F&& as a template parameter when the callable is used immediately (algorithms, visitors), store std::function when you need to keep callbacks of varying types (event handlers, task queues), and use plain function pointers at C boundaries or when you need a trivially copyable, constexpr-friendly type such as an entry in a static dispatch table.


Member Function Pointer

class Calculator {
public:
    int add(int a, int b) {
        return a + b;
    }
    
    int multiply(int a, int b) {
        return a * b;
    }
};

int main() {
    Calculator calc;
    
    // Member function pointer
    int (Calculator::*funcPtr)(int, int) = &Calculator::add;
    
    // Call
    int result = (calc.*funcPtr)(3, 4);  // 7
    std::cout << result << std::endl;
    
    // Call with pointer
    Calculator* ptr = &calc;
    result = (ptr->*funcPtr)(3, 4);
    std::cout << result << std::endl;
}

A pointer to member function is not an address you can call on its own; it is more like an offset that needs an object. Non-static member functions take a hidden this parameter, so the call always supplies an object with .* or ->*. The extra parentheses in (calc.*funcPtr)(3, 4) are required because the call operator binds tighter than .*; without them, calc.*funcPtr(3, 4) tries to call funcPtr first and fails to compile.

Member function pointers are also bigger and more complex than ordinary function pointers. To support virtual functions and multiple inheritance, implementations may store a vtable offset and a this adjustment alongside the address, so sizeof(int (Calculator::*)(int, int)) is commonly 16 bytes with GCC and Clang, and it can vary with the class’s inheritance on MSVC. They cannot be converted to void* or to ordinary function pointers. If add were const, the pointer type would need const too: int (Calculator::*)(int, int) const. In modern code, std::invoke(funcPtr, calc, 3, 4) calls a member pointer with the same syntax as any other callable, and std::mem_fn or a lambda ([&calc](int a, int b) { return calc.add(a, b); }) is usually clearer than raw member-pointer syntax.


Common Issues

Issue 1: Syntax Complexity

// ❌ Complex syntax
int (*funcPtr)(int, int);

// ✅ Simplify with using
using Operation = int(*)(int, int);
Operation funcPtr;

Issue 2: nullptr Check

using Callback = void(*)();

void execute(Callback cb) {
    // ❌ No nullptr check
    // cb();  // Undefined behavior if cb is null (usually a crash at address 0)
    
    // ✅ nullptr check
    if (cb) {
        cb();
    }
}

Calling a null function pointer jumps to address 0, which on desktop operating systems is an unmapped page and produces an immediate segmentation fault. The stack trace is often unhelpful, because the crash happens at address 0x0 rather than inside any function, and debuggers show the caller one frame up. On embedded systems without memory protection, address 0 may contain valid code or the reset vector, and the “call” does something bizarre instead of crashing. Whether the check belongs in execute or at registration time is a design choice: if a missing callback is legitimate, check at the call; if it is a bug, reject nullptr when the callback is set so the error surfaces where it was introduced.

Issue 3: Lambda Capture

int x = 10;

// ❌ Lambda with capture cannot be function pointer
// void (*ptr)() = [x]() { std::cout << x; };  // Error

// ✅ Use std::function
std::function<void()> func = [x]() { std::cout << x; };

A lambda is an object of a unique class type. If it captures nothing, that class provides a conversion to a plain function pointer, because the lambda’s code does not need any data. Once it captures x, the call needs that captured value, and a bare function pointer has nowhere to carry it, so the conversion does not exist and you get an error such as cannot convert '<lambda()>' to 'void (*)()' in initialization. Writing +[]() { ... } forces the conversion for a captureless lambda, which occasionally helps with template deduction. When a C API needs a function pointer but you have state, the void* userData pattern from Example 1 is the bridge: pass a captureless lambda as the callback and a pointer to your state as the user data.

Issue 4: Member Function Pointer

class MyClass {
public:
    void func() {}
};

// ❌ Cannot use regular function pointer
// void (*ptr)() = &MyClass::func;  // Error

// ✅ Member function pointer
void (MyClass::*ptr)() = &MyClass::func;

MyClass obj;
(obj.*ptr)();

Function Pointer Array

using Operation = int(*)(int, int);

int add(int a, int b) { return a + b; }
int subtract(int a, int b) { return a - b; }
int multiply(int a, int b) { return a * b; }
int divide(int a, int b) { return b != 0 ? a / b : 0; }

int main() {
    Operation operations[] = {add, subtract, multiply, divide};
    
    for (int i = 0; i < 4; i++) {
        std::cout << operations[i](10, 5) << " ";
    }
    std::cout << std::endl;  // 15 5 50 2
}

An array of function pointers is the classic implementation of a dispatch table indexed by a small integer, such as an opcode in an interpreter or a state number in a state machine: operations[op](a, b) replaces a switch. Because every element is a plain pointer, the table can be constexpr or static const and placed in read-only memory, which is common in embedded code. The loop uses the literal 4; std::size(operations) or a range-for keeps it correct if an operation is added. Indexing with an unchecked value from input is the main risk: an out-of-range index reads a garbage “function pointer” and jumps to it, which is both a crash and a well-known security vulnerability pattern, so validate the index before calling.


std::function vs Function Pointer

#include <functional>

int add(int a, int b) { return a + b; }

int main() {
    // Function pointer: Functions only
    int (*ptr)(int, int) = add;
    
    // std::function: Functions, lambdas, function objects all
    std::function<int(int, int)> func1 = add;
    std::function<int(int, int)> func2 = [](int a, int b) { return a * b; };
    
    struct Multiplier {
        int operator()(int a, int b) const {
            return a * b;
        }
    };
    std::function<int(int, int)> func3 = Multiplier();
}

FAQ

Q1: When to use function pointer?

A:

  • Callback functions
  • Function tables
  • Plugin systems

Q2: std::function vs function pointer?

A:

  • Function pointer: Fast, functions only
  • std::function: Flexible, lambdas/function objects

Q3: Performance?

A: A call through a function pointer is an indirect call: a few nanoseconds at most, similar to a virtual call, and usually well predicted by the CPU when the target does not change. The larger cost is that the compiler usually cannot inline through it. For hot inner loops, pass a lambda or function object to a template instead; elsewhere the difference is rarely measurable.

Q4: Member function pointer?

A: Different syntax. &Class::func, (obj.*ptr)()

Q5: Need nullptr check?

A: Yes. Check recommended for safety.

Q6: Function pointer learning resources?

A:

  • “C++ Primer”
  • cppreference.com
  • “Effective C++“