Function Pointers
Functions are values.
That means a function can be:
- stored
- passed to another function
- selected dynamically
- called indirectly
A function pointer stores the address of a function.
Instead of calling a function directly:
add(10, 20);you can store the function in a variable and call it later.
This is called indirect function calling.
Function pointers are important in:
- callbacks
- event systems
- plugin systems
- virtual machines
- game engines
- operating systems
- schedulers
- interpreters
A First Example
const std = @import("std");
fn add(a: i32, b: i32) i32 {
return a + b;
}
pub fn main() void {
const operation = add;
const result = operation(10, 20);
std.debug.print("{}\n", .{result});
}Output:
30This line:
const operation = add;stores the function in a variable.
Then:
operation(10, 20);calls the function indirectly.
Functions Have Types
Every function has a function type.
Example:
fn add(a: i32, b: i32) i32This means:
- parameters:
i32i32
- returns:
i32
A matching function pointer must use the same signature.
Explicit Function Pointer Types
You can declare function pointer types explicitly.
Example:
const Operation = *const fn(i32, i32) i32;Read this carefully:
| Part | Meaning |
|---|---|
*const | pointer to immutable function |
fn(...) | function type |
(i32, i32) | parameter types |
i32 | return type |
Using it:
const std = @import("std");
const Operation = *const fn(i32, i32) i32;
fn add(a: i32, b: i32) i32 {
return a + b;
}
pub fn main() void {
const op: Operation = add;
const result = op(5, 7);
std.debug.print("{}\n", .{result});
}Output:
12Selecting Functions Dynamically
Function pointers allow runtime selection.
Example:
const std = @import("std");
fn add(a: i32, b: i32) i32 {
return a + b;
}
fn subtract(a: i32, b: i32) i32 {
return a - b;
}
pub fn main() void {
const use_add = true;
const op = if (use_add)
add
else
subtract;
const result = op(20, 5);
std.debug.print("{}\n", .{result});
}Output:
25Changing:
const use_add = false;produces:
15The selected function changes dynamically.
Passing Functions to Functions
Functions can receive function pointers as parameters.
Example:
const std = @import("std");
fn add(a: i32, b: i32) i32 {
return a + b;
}
fn multiply(a: i32, b: i32) i32 {
return a * b;
}
fn execute(
op: *const fn(i32, i32) i32,
a: i32,
b: i32,
) i32 {
return op(a, b);
}
pub fn main() void {
const x = execute(add, 2, 3);
const y = execute(multiply, 2, 3);
std.debug.print("{} {}\n", .{ x, y });
}Output:
5 6This is a callback pattern.
The caller supplies behavior.
Callback Mental Model
Without callbacks:
function decides behaviorWith callbacks:
caller provides behaviorThis makes programs more flexible.
Arrays of Function Pointers
You can store multiple functions together.
Example:
const std = @import("std");
fn add(a: i32, b: i32) i32 {
return a + b;
}
fn subtract(a: i32, b: i32) i32 {
return a - b;
}
fn multiply(a: i32, b: i32) i32 {
return a * b;
}
pub fn main() void {
const operations = [_]*const fn(i32, i32) i32{
add,
subtract,
multiply,
};
for (operations) |op| {
const result = op(10, 5);
std.debug.print("{}\n", .{result});
}
}Output:
15
5
50This pattern appears in interpreters and dispatch systems.
Dispatch Tables
Function pointer arrays are often called dispatch tables.
Conceptually:
opcode -> functionExample:
ADD -> addFunction
SUB -> subtractFunction
MUL -> multiplyFunctionVirtual machines frequently work this way.
Function Pointers Inside Structs
Structs may contain function pointers.
Example:
const Handler = struct {
callback: *const fn() void,
};This allows objects to carry behavior.
Example usage:
const Button = struct {
onClick: *const fn() void,
};GUI systems often use this style.
Returning Function Pointers
Functions may return function pointers.
Example:
fn choose(
flag: bool,
) *const fn(i32, i32) i32 {
if (flag) {
return add;
}
return subtract;
}This allows dynamic strategy selection.
Anonymous Functions and Closures
Some languages support closures heavily.
Zig intentionally keeps things simpler.
Zig does not have full automatic closures like JavaScript or Python.
Instead, Zig prefers explicit data passing.
This reduces hidden allocations and hidden state.
Compile-Time Function Selection
Zig often performs function selection at compile time.
Example:
fn process(comptime op: fn(i32, i32) i32) void {
}This allows the compiler to optimize aggressively.
Compile-time polymorphism is one of Zig’s major strengths.
Function Pointer Safety
Function pointers must match the correct signature.
Incorrect example:
fn hello() void {
}
const op: *const fn(i32, i32) i32 = hello;Compiler error:
function type mismatchThe compiler checks signatures strictly.
Direct Calls vs Indirect Calls
Direct call:
add(1, 2);Indirect call:
op(1, 2);Indirect calls are slightly harder for CPUs to optimize because the exact target may not be known ahead of time.
Sometimes performance-critical systems avoid excessive indirect calls.
Function Pointers in Real Systems
Function pointers are common in:
| System | Usage |
|---|---|
| GUI toolkits | button callbacks |
| Game engines | event handlers |
| Operating systems | interrupt tables |
| Interpreters | opcode dispatch |
| Networking | packet handlers |
| Databases | query execution |
| Plugins | dynamic behaviors |
They are one of the core building blocks of low-level software design.
A Complete Example
const std = @import("std");
const Operation = *const fn(i32, i32) i32;
fn add(a: i32, b: i32) i32 {
return a + b;
}
fn subtract(a: i32, b: i32) i32 {
return a - b;
}
fn calculate(
op: Operation,
a: i32,
b: i32,
) i32 {
return op(a, b);
}
pub fn main() void {
const operations = [_]Operation{
add,
subtract,
};
for (operations) |op| {
const result = calculate(op, 20, 5);
std.debug.print("{}\n", .{result});
}
}Output:
25
15This program demonstrates:
- storing functions
- passing functions
- indirect calls
- dispatch through arrays
These are foundational techniques in systems programming.
Mental Model
A function pointer is:
a variable that refers to executable codeInstead of storing ordinary data like integers or strings:
42
"hello"a function pointer stores:
where a function lives in memoryThis allows programs to choose behavior dynamically.
That flexibility is one reason function pointers are so important in low-level programming.