Skip to content

Zig Generics Use `comptime`

A generic function is a function that works with many types instead of only one type.

Generic Functions

A generic function is a function that works with many types instead of only one type.

Without generics, you often duplicate code.

Example:

fn addI32(a: i32, b: i32) i32 {
    return a + b;
}

Then later:

fn addF64(a: f64, b: f64) f64 {
    return a + b;
}

The logic is identical.

Only the types change.

Generic functions solve this problem.

Zig Generics Use comptime

Zig implements generics through compile-time programming.

The most common pattern uses:

comptime

Example:

fn add(
    comptime T: type,
    a: T,
    b: T,
) T {
    return a + b;
}

This function works with many types.

Calling Generic Functions

Example:

const x = add(i32, 10, 20);
const y = add(f64, 1.5, 2.5);

Results:

x = 30
y = 4.0

The compiler generates specialized versions for each type.

Conceptually:

add(i32, ...)
  -> integer version

add(f64, ...)
  -> floating-point version

Understanding comptime T: type

This line:

comptime T: type

means:

  • T is known at compile time
  • T itself is a type

The caller passes a type as input.

Example:

add(i32, 1, 2)

Here:

T = i32

The compiler creates a version specialized for i32.

Why Zig Uses Compile-Time Generics

Many languages implement generics differently.

Examples:

LanguageGeneric System
C++templates
Rustmonomorphization + traits
Javatype erasure
Gotype parameters
Zigcompile-time execution

Zig’s system is unusually flexible because generics are built from normal compile-time language features.

A Generic Maximum Function

Example:

fn max(
    comptime T: type,
    a: T,
    b: T,
) T {
    return if (a > b) a else b;
}

Calling:

const a = max(i32, 10, 20);
const b = max(f64, 1.5, 0.5);

Results:

a = 20
b = 1.5

Generic Arrays

Generics work well with arrays.

Example:

fn first(
    comptime T: type,
    values: []const T,
) T {
    return values[0];
}

Calling:

const ints = [_]i32{ 1, 2, 3 };
const floats = [_]f64{ 1.5, 2.5 };

const a = first(i32, &ints);
const b = first(f64, &floats);

The same logic works for many element types.

Generic Structs

Generics are not limited to functions.

Structs can also be generic.

Example:

fn Box(comptime T: type) type {
    return struct {
        value: T,
    };
}

Using it:

const IntBox = Box(i32);

const box = IntBox{
    .value = 123,
};

This generates a custom structure specialized for i32.

Generic Data Structures

Generic structs are extremely important.

Examples:

  • lists
  • hash maps
  • queues
  • trees
  • allocators
  • buffers

The Zig standard library uses generics heavily.

Generic Printing

Example:

fn printValue(value: anytype) void {
    std.debug.print("{}\n", .{value});
}

This uses:

anytype

which allows automatic generic parameter inference.

Calling:

printValue(123);
printValue(3.14);
printValue(true);

The compiler determines the types automatically.

anytype

anytype is a convenient shorthand.

Instead of:

fn print(
    comptime T: type,
    value: T,
)

you can write:

fn print(value: anytype)

This is common in Zig APIs.

Type Inference

Example:

fn identity(value: anytype) @TypeOf(value) {
    return value;
}

Calling:

const x = identity(10);
const y = identity(3.5);

The compiler infers types automatically.

Compile-Time Specialization

Each type creates a specialized version.

Conceptually:

add(i32, ...)
  -> generated code #1

add(f64, ...)
  -> generated code #2

This often produces very efficient machine code.

Generic Constraints

Sometimes not all types are valid.

Example:

fn add(
    comptime T: type,
    a: T,
    b: T,
) T {
    return a + b;
}

This works for numeric types.

But calling:

add([]const u8, "a", "b");

fails because strings cannot use + this way.

The compiler checks operations during specialization.

Compile-Time Type Inspection

Zig can inspect types at compile time.

Example:

fn describe(
    comptime T: type,
) void {
    std.debug.print(
        "{any}\n",
        .{@typeInfo(T)},
    );
}

Calling:

describe(i32);

This is part of Zig’s reflection system.

Generic Algorithms

Generic algorithms are one of the biggest uses of generics.

Example conceptual algorithms:

  • sorting
  • searching
  • hashing
  • parsing
  • serialization

One algorithm can support many types.

Generic Memory Utilities

Example conceptual function:

fn swap(
    comptime T: type,
    a: *T,
    b: *T,
) void {

}

This can swap:

  • integers
  • floats
  • structs
  • arrays

without duplicating code.

Generics and Zero-Cost Abstractions

Zig generics are designed for high performance.

The compiler generates specialized code directly.

This avoids many runtime costs.

Conceptually:

generic abstraction
  ->
specialized machine code

This is often called a zero-cost abstraction.

Generic Iteration Example

fn printAll(values: anytype) void {
    for (values) |value| {
        std.debug.print(
            "{}\n",
            .{value},
        );
    }
}

Calling:

const numbers = [_]i32{ 1, 2, 3 };

printAll(numbers);

The compiler adapts the function automatically.

Generic Errors

Generic code can produce confusing compiler messages initially.

Example:

fn test(value: anytype) void {
    value.invalidMethod();
}

Compiler errors may appear during specialization instead of initial parsing.

This is normal in compile-time generic systems.

Generics vs Runtime Polymorphism

Generics:

compile-time specialization

Function pointers/interfaces:

runtime polymorphism

Generics usually produce faster code because decisions happen during compilation.

Runtime polymorphism is more dynamic but may cost more at runtime.

A Complete Example

const std = @import("std");

fn swap(
    comptime T: type,
    a: *T,
    b: *T,
) void {
    const temp = a.*;
    a.* = b.*;
    b.* = temp;
}

pub fn main() void {
    var x: i32 = 10;
    var y: i32 = 20;

    swap(i32, &x, &y);

    std.debug.print(
        "{} {}\n",
        .{ x, y },
    );

    var a: f64 = 1.5;
    var b: f64 = 2.5;

    swap(f64, &a, &b);

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

Output:

20 10
2.5 1.5

One function works for multiple types safely and efficiently.

Mental Model

Generic functions mean:

write logic once
use it with many types

Zig achieves this through compile-time specialization.

The core ideas are:

FeaturePurpose
comptimecompile-time parameters
typetypes as values
anytypeinferred generic parameters
specializationgenerated type-specific code

Generics are one of Zig’s most powerful features because they combine:

  • flexibility
  • strong type safety
  • compile-time checking
  • high performance

without requiring a separate template language or heavy runtime system.