Skip to content

Inline Loops

A normal loop runs while the program runs.

A normal loop runs while the program runs.

An inline loop is expanded while the program is compiled.

inline for (.{ 1, 2, 3 }) |n| {
    std.debug.print("{d}\n", .{n});
}

This is close to writing:

std.debug.print("{d}\n", .{1});
std.debug.print("{d}\n", .{2});
std.debug.print("{d}\n", .{3});

The loop itself does not exist at runtime in the same way. The compiler uses the loop to generate repeated code.

Inline loops are most useful when each iteration must be known at compile time.

A common case is iterating over a tuple.

const std = @import("std");

pub fn main() void {
    const values = .{ 10, true, "zig" };

    inline for (values) |v| {
        std.debug.print("{any}\n", .{v});
    }
}

The tuple contains values of different types.

A normal for loop cannot handle this as one runtime sequence, because each element has a different type.

The inline loop works because the compiler expands each iteration separately.

The generated code is like:

std.debug.print("{any}\n", .{10});
std.debug.print("{any}\n", .{true});
std.debug.print("{any}\n", .{"zig"});

Each call is checked with the exact type of its value.

Inline loops are also useful with types.

const std = @import("std");

pub fn main() void {
    inline for (.{ i8, i16, i32, i64 }) |T| {
        std.debug.print("{s}: {d} bytes\n", .{
            @typeName(T),
            @sizeOf(T),
        });
    }
}

Typical output:

i8: 1 bytes
i16: 2 bytes
i32: 4 bytes
i64: 8 bytes

The loop variable T is a type value. It exists at compile time.

A normal loop cannot range over types. Types are not runtime objects.

Inline loops can be used inside generic code.

fn hasField(comptime T: type, comptime name: []const u8) bool {
    inline for (@typeInfo(T).@"struct".fields) |field| {
        if (std.mem.eql(u8, field.name, name))
            return true;
    }

    return false;
}

This walks the fields of a struct while compiling.

For example:

const Point = struct {
    x: i32,
    y: i32,
};

comptime {
    if (!hasField(Point, "x"))
        @compileError("Point must have field x");
}

The check happens before the program can run.

Inline loops can also build repeated tests.

test "integer sizes" {
    inline for (.{ u8, u16, u32, u64 }) |T| {
        try std.testing.expect(@sizeOf(T) * 8 == @typeInfo(T).int.bits);
    }
}

Each iteration is its own compile-time expansion.

Use inline for when the loop body depends on compile-time information.

Use an ordinary for when the loop is merely processing runtime data.

This is ordinary runtime code:

for (items) |item| {
    sum += item;
}

This is compile-time code generation:

inline for (fields) |field| {
    // generate field-specific code
}

Inline loops should be used carefully. They duplicate code. If the sequence is large, the generated program may become larger.

The rule is simple: use an inline loop when the compiler must see each iteration separately.

Exercise 10-13. Use inline for to print the names and sizes of u8, u16, u32, and u64.

Exercise 10-14. Write a function that checks whether a struct has a field with a given name.

Exercise 10-15. Use inline for over a tuple containing values of different types.

Exercise 10-16. Rewrite a normal loop as an inline loop and compare the two cases.