# Compile-Time Dispatch

### Compile-Time Dispatch

Generic functions in Zig are specialized at compile time.

When a generic function is called with concrete types, the compiler generates a version of the function for those types. The selection happens during compilation, not at runtime.

Consider this function:

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

fn square(x: anytype) @TypeOf(x) {
    return x * x;
}

pub fn main() void {
    const a = square(4);
    const b = square(2.5);

    std.debug.print("{d}\n", .{a});
    std.debug.print("{d}\n", .{b});
}
```

The output is:

```text
16
6.25
```

The compiler creates separate versions of `square`:

```zig
square(i32)
square(f64)
```

Each uses operations appropriate for the concrete type.

There is no runtime type inspection. No hidden dispatch table exists. The generated machine code is direct and specialized.

This is compile-time dispatch.

The mechanism becomes more important with structs.

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

const Dog = struct {
    pub fn speak(self: *Dog) void {
        _ = self;
        std.debug.print("woof\n", .{});
    }
};

const Cat = struct {
    pub fn speak(self: *Cat) void {
        _ = self;
        std.debug.print("meow\n", .{});
    }
};

fn speak(animal: anytype) void {
    animal.speak();
}

pub fn main() void {
    var dog = Dog{};
    var cat = Cat{};

    speak(&dog);
    speak(&cat);
}
```

The output is:

```text
woof
meow
```

The function:

```zig
fn speak(animal: anytype)
```

is instantiated separately for:

```zig
*Dog
*Cat
```

The compiler resolves the method call statically.

This differs from virtual dispatch in languages such as C++ or Java.

In Zig:

- the concrete type is usually known
- specialization happens during compilation
- the generated call is direct

The result is efficient machine code with little abstraction overhead.

Compile-time dispatch also enables type-dependent logic.

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

fn describe(value: anytype) void {
    const T = @TypeOf(value);

    switch (@typeInfo(T)) {
        .int => std.debug.print("integer\n", .{}),
        .float => std.debug.print("float\n", .{}),
        else => std.debug.print("other\n", .{}),
    }
}

pub fn main() void {
    describe(10);
    describe(3.14);
    describe(true);
}
```

The output is:

```text
integer
float
other
```

The `switch` executes during compilation because the type information is known at compile time.

This allows a single function body to generate different code depending on the argument type.

Compile-time dispatch is heavily used in the standard library.

Examples include:

- formatting
- serialization
- allocators
- containers
- parsers
- testing utilities

The formatter in `std.debug.print` examines argument types during compilation and generates formatting logic specialized for those types.

For example:

```zig
std.debug.print("{d}\n", .{123});
std.debug.print("{s}\n", .{"zig"});
```

The formatting behavior is selected at compile time from the argument types.

Compile-time dispatch can also remove unused branches entirely.

```zig
fn process(comptime debug: bool) void {
    if (debug) {
        @compileLog("debug enabled");
    }
}
```

When `debug` is known at compile time, the compiler keeps only the selected branch.

This is different from an ordinary runtime condition.

Compile-time dispatch therefore serves several purposes:

- selecting operations by type
- generating specialized code
- eliminating unused branches
- implementing generic abstractions efficiently

The language relies on compile-time specialization instead of large runtime object systems.

Exercise 11-17. Write a generic function that prints different messages for integers and floats.

Exercise 11-18. Write a generic function that behaves differently for signed and unsigned integers.

Exercise 11-19. Implement a generic formatter for a small struct.

Exercise 11-20. Write a compile-time boolean parameter that enables or disables logging code.

