Skip to content

Generic Functions

A function in Zig can take types as parameters.

A function in Zig can take types as parameters.

This is the basis of generic programming in the language. Instead of writing separate functions for integers, floating-point values, or user-defined types, a single function can operate on many kinds of values.

Here is a simple example:

const std = @import("std");

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

pub fn main() void {
    const x = add(i32, 3, 4);
    const y = add(f64, 1.5, 2.25);

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

The output is:

7
3.75

The first parameter is unusual:

comptime T: type

T is not a normal runtime value. It is known at compile time.

The type of T is:

type

In Zig, types themselves are values available during compilation.

The call:

add(i32, 3, 4)

passes the integer type i32 into the function.

Inside the function, the parameters become:

a: i32
b: i32

and the return type becomes:

i32

The compiler generates a specialized version of the function for that type.

The second call:

add(f64, 1.5, 2.25)

creates another specialization using f64.

The function body is written once:

return a + b;

but the generated machine code depends on the concrete type.

A generic function may also infer types from arguments.

This version does not require the caller to pass T explicitly:

const std = @import("std");

fn max(a: anytype, b: anytype) @TypeOf(a, b) {
    return if (a > b) a else b;
}

pub fn main() void {
    const x = max(10, 20);
    const y = max(3.5, 1.25);

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

The output is:

20
3.5

anytype means the compiler should infer the parameter type from the argument.

The return type is:

@TypeOf(a, b)

This builtin asks the compiler to compute a common type from the arguments.

Generic functions are compiled lazily. Zig does not generate machine code for every possible type. A specialization appears only when the function is used.

This program is legal:

fn square(comptime T: type, x: T) T {
    return x * x;
}

pub fn main() void {}

Even if square contains invalid operations for some types, no error occurs until the function is instantiated with such a type.

For example:

const std = @import("std");

fn square(comptime T: type, x: T) T {
    return x * x;
}

pub fn main() void {
    const s = square([]const u8, "zig");
    _ = s;
}

fails because slices cannot be multiplied.

The compiler reports the error only after the function is used with that type.

Generic programming in Zig is based on ordinary language features:

  • compile-time execution
  • types as values
  • normal function calls
  • specialization during compilation

There is no separate template language.

Exercise 11-1. Write a generic min function.

Exercise 11-2. Write a generic swap function using pointers.

Exercise 11-3. Write a generic function that returns the larger of three values.

Exercise 11-4. Modify add so it works only for integer types.