# Static Dispatch

### Static Dispatch

Static dispatch means the compiler decides which code to call before the program runs.

This is different from dynamic dispatch, where the program decides which code to call while it is running.

In Zig, static dispatch is common because types are known at compile time. When a function receives a `comptime T: type`, the compiler can specialize the function for that exact type.

#### A Simple Example

Look at this function:

```zig
fn double(comptime T: type, value: T) T {
    return value + value;
}
```

Use it like this:

```zig
const a = double(i32, 10);
const b = double(f64, 1.5);
```

The compiler knows that the first call uses `i32`.

It also knows that the second call uses `f64`.

So it can produce code specialized for each type.

There is no runtime question like:

```text
What type is this value?
```

The compiler already knows.

#### Runtime Dispatch

Runtime dispatch happens when the program chooses behavior while running.

For example, imagine a command-line program:

```zig
if (user_choice == 1) {
    runFastMode();
} else {
    runSafeMode();
}
```

The program cannot know `user_choice` until runtime. The user enters it after the program starts.

So the branch must exist in the final program.

The CPU checks the condition while the program runs.

#### Static Dispatch

Static dispatch happens when the choice is known during compilation.

```zig
fn runMode(comptime fast: bool) void {
    if (fast) {
        runFastMode();
    } else {
        runSafeMode();
    }
}
```

Usage:

```zig
runMode(true);
```

The compiler knows `fast` is `true`.

So it can keep the `runFastMode()` path for that call.

The choice has already been made before runtime.

#### Dispatch by Type

Static dispatch is often based on type.

```zig
const std = @import("std");

fn printValue(comptime T: type, value: T) void {
    if (T == bool) {
        std.debug.print("bool: {}\n", .{value});
    } else if (T == i32) {
        std.debug.print("i32: {}\n", .{value});
    } else if (T == []const u8) {
        std.debug.print("string: {s}\n", .{value});
    } else {
        @compileError("unsupported type");
    }
}
```

Usage:

```zig
printValue(bool, true);
printValue(i32, 123);
printValue([]const u8, "hello");
```

The compiler sees each type and chooses the correct branch.

For `bool`, it uses the `bool` path.

For `i32`, it uses the `i32` path.

For `[]const u8`, it uses the string path.

There is no runtime type lookup.

#### Static Dispatch with `switch`

A `switch` is often clearer than a long chain of `if` statements.

```zig
fn defaultValue(comptime T: type) T {
    return switch (T) {
        bool => false,
        u8 => 0,
        i32 => 0,
        f64 => 0.0,
        else => @compileError("unsupported type"),
    };
}
```

Usage:

```zig
const a = defaultValue(bool);
const b = defaultValue(i32);
```

The compiler chooses the branch from the type `T`.

The result type also depends on `T`.

For `defaultValue(bool)`, the return type is `bool`.

For `defaultValue(i32)`, the return type is `i32`.

#### Static Dispatch Avoids Runtime Type Tags

Some languages store runtime type information with values.

That allows the program to ask:

```text
What kind of value is this?
```

Then the program chooses behavior dynamically.

Zig usually avoids this when the type is known at compile time.

Instead of carrying a runtime type tag, Zig lets the compiler specialize the code.

This can make the runtime code smaller, simpler, and faster.

But it also means the program must know the type before runtime.

#### Static Dispatch in Generic Containers

Static dispatch is used heavily in generic containers.

```zig
fn Box(comptime T: type) type {
    return struct {
        value: T,

        pub fn get(self: @This()) T {
            return self.value;
        }
    };
}
```

Usage:

```zig
const IntBox = Box(i32);
const BoolBox = Box(bool);
```

`Box(i32)` and `Box(bool)` are separate concrete types.

The compiler knows the field type and method return type for each one.

There is no shared runtime object that stores “this box contains an i32” or “this box contains a bool.”

The type itself already says that.

#### Static Dispatch with Interfaces

Zig does not have traditional object-oriented interfaces.

Instead, Zig often uses compile-time checks.

Suppose we want a function that works with any type that has a `write` method.

```zig
fn writeHello(writer: anytype) !void {
    try writer.writeAll("hello");
}
```

Here, `anytype` means the compiler infers the concrete type at the call site.

If the passed value has a compatible `writeAll` method, the code compiles.

If not, compilation fails.

Example:

```zig
try writeHello(file.writer());
```

The compiler knows the concrete writer type returned by `file.writer()`.

Then it checks whether that type supports `writeAll`.

This is static dispatch. The method call is resolved from the concrete type during compilation.

#### `anytype` Is Compile-Time Generic

`anytype` is a shorthand for a compile-time generic parameter.

This:

```zig
fn printTwice(value: anytype) void {
    @import("std").debug.print("{} {}\n", .{ value, value });
}
```

behaves like a generic function.

The compiler creates a version for each concrete type you use.

```zig
printTwice(10);
printTwice(true);
```

The first call uses an integer type.

The second call uses `bool`.

Each call is checked separately.

#### Compile-Time Interface Checks

You can write explicit checks for expected methods or declarations.

For example, `@hasDecl` checks whether a type has a declaration.

```zig
fn requireReset(comptime T: type) void {
    if (!@hasDecl(T, "reset")) {
        @compileError("type must provide reset");
    }
}
```

Use it inside a generic function:

```zig
fn resetAll(items: anytype) void {
    const Slice = @TypeOf(items);
    const info = @typeInfo(Slice);

    if (info != .pointer) {
        @compileError("expected a slice");
    }

    const Child = info.pointer.child;
    requireReset(Child);

    for (items) |*item| {
        item.reset();
    }
}
```

The compiler checks the element type before runtime.

If the element type does not provide `reset`, the program fails to compile.

#### Static Dispatch Can Produce Clear Errors

Static dispatch catches mistakes early.

Example:

```zig
const Counter = struct {
    value: u32,

    pub fn reset(self: *@This()) void {
        self.value = 0;
    }
};

const User = struct {
    name: []const u8,
};
```

This works:

```zig
var counters = [_]Counter{
    .{ .value = 1 },
    .{ .value = 2 },
};

resetAll(counters[0..]);
```

This fails:

```zig
var users = [_]User{
    .{ .name = "Ada" },
};

resetAll(users[0..]);
```

`User` has no `reset` method, so the compiler rejects the call.

That is the main advantage: the error appears before the program runs.

#### Static Dispatch vs Dynamic Dispatch

Both styles are useful.

| Style | Decision Time | Good For |
|---|---|---|
| Static dispatch | Compile time | Generics, known types, zero-overhead abstractions |
| Dynamic dispatch | Runtime | Plugin systems, heterogeneous collections, late binding |

Static dispatch is excellent when the compiler knows the concrete type.

Dynamic dispatch is useful when the program must choose behavior from runtime data.

For example, a plugin loaded from disk at runtime cannot usually be selected purely at compile time.

A command parsed from user input is also runtime data.

#### Static Dispatch and Performance

Static dispatch can help performance because the compiler sees the exact function being called.

That can allow:

| Optimization | Meaning |
|---|---|
| Inlining | Put function body directly at the call site |
| Constant folding | Compute known values early |
| Dead code removal | Remove unused branches |
| Specialization | Generate code for a specific type |
| Better register use | Optimize with concrete layout information |

But do not use static dispatch only because it sounds faster.

Use it when the decision naturally belongs to compile time.

#### A Practical Example: Generic `min`

Here is a small generic `min` function:

```zig
fn min(comptime T: type, a: T, b: T) T {
    return if (a < b) a else b;
}
```

Usage:

```zig
const x = min(i32, 10, 20);
const y = min(f64, 3.5, 1.25);
```

The compiler checks each call using the concrete type.

If you try a type that cannot be compared with `<`, compilation fails.

```zig
const z = min(bool, true, false);
```

This is rejected because `<` does not apply to `bool`.

The generic function is not loosely typed. It is checked after specialization.

#### A Practical Example: Compile-Time Strategy

You can choose a strategy at compile time.

```zig
const SortMode = enum {
    fast,
    stable,
};

fn sort(comptime mode: SortMode, items: []i32) void {
    switch (mode) {
        .fast => quickSort(items),
        .stable => mergeSort(items),
    }
}
```

Usage:

```zig
sort(.fast, numbers);
```

The compiler knows the mode.

The generated code can use the selected branch directly.

Use this when the caller should decide the strategy in source code, not from user input.

#### When Static Dispatch Is the Wrong Tool

Static dispatch is wrong when the choice depends on runtime facts.

Bad fit:

```zig
const mode = readModeFromUser();
sort(mode, numbers);
```

If `mode` comes from the user, it is runtime data.

The function should accept a normal runtime value instead:

```zig
fn sort(mode: SortMode, items: []i32) void {
    switch (mode) {
        .fast => quickSort(items),
        .stable => mergeSort(items),
    }
}
```

Now the program chooses the branch while running.

That is correct because the information arrives while running.

#### Mental Model

Static dispatch means:

```text
The compiler knows the exact path before the program runs.
```

Dynamic dispatch means:

```text
The running program chooses the path.
```

Zig gives you strong tools for static dispatch through `comptime`, `anytype`, generic type functions, `inline for`, `@typeInfo`, and compile-time checks.

Use static dispatch when the choice is part of the program’s structure.

Use runtime dispatch when the choice comes from the outside world.

