Skip to content

A First Example

Functions are values.

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:

30

This 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) i32

This means:

  • parameters:
    • i32
    • i32
  • 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:

PartMeaning
*constpointer to immutable function
fn(...)function type
(i32, i32)parameter types
i32return 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:

12

Selecting 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:

25

Changing:

const use_add = false;

produces:

15

The 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 6

This is a callback pattern.

The caller supplies behavior.

Callback Mental Model

Without callbacks:

function decides behavior

With callbacks:

caller provides behavior

This 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
50

This pattern appears in interpreters and dispatch systems.

Dispatch Tables

Function pointer arrays are often called dispatch tables.

Conceptually:

opcode -> function

Example:

ADD -> addFunction
SUB -> subtractFunction
MUL -> multiplyFunction

Virtual 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 mismatch

The 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:

SystemUsage
GUI toolkitsbutton callbacks
Game enginesevent handlers
Operating systemsinterrupt tables
Interpretersopcode dispatch
Networkingpacket handlers
Databasesquery execution
Pluginsdynamic 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
15

This 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 code

Instead of storing ordinary data like integers or strings:

42
"hello"

a function pointer stores:

where a function lives in memory

This allows programs to choose behavior dynamically.

That flexibility is one reason function pointers are so important in low-level programming.