⚡ Zig Guide LiveUnofficialbut fully verified
✓ Zig 0.17.0-dev.1509+bb296ab9bWhat's newOn an older Zig?

Array Recipes

Three things people reach for once arrays stop being toy examples: a grid, a table computed at compile time, and a fixed-size array taken back out of a slice. All three are language mechanics. None of them needs an allocator.

const std = @import("std");
const expect = std.testing.expect;

test "a grid is an array of arrays" {
    const grid = [3][3]u8{
        .{ 1, 2, 3 },
        .{ 4, 5, 6 },
        .{ 7, 8, 9 },
    };

    // Both lengths are part of the type, and the whole thing is one
    // contiguous block: nine bytes, no pointers anywhere.
    try expect(grid.len == 3); // rows
    try expect(grid[0].len == 3); // columns
    try expect(@TypeOf(grid[0]) == [3]u8);
    try expect(@sizeOf(@TypeOf(grid)) == 9);

    try expect(grid[1][2] == 6); // row first, then column
}

test "walk a grid with nested for" {
    const grid = [2][3]u8{
        .{ 1, 2, 3 },
        .{ 4, 5, 6 },
    };

    var sum: u32 = 0;
    for (grid) |row| {
        for (row) |cell| sum += cell;
    }
    try expect(sum == 21);
}

test "the outer loop hands you a copy" {
    var grid = [2][2]u8{
        .{ 1, 2 },
        .{ 3, 4 },
    };

    // Arrays are values, so `row` is a copy and writing to it changes nothing.
    for (grid) |row| {
        var mutable = row;
        mutable[0] = 99;
    }
    try expect(grid[0][0] == 1);

    // Take pointers at both levels to write through.
    for (&grid) |*row| {
        for (row) |*cell| cell.* += 10;
    }
    try expect(grid[0][0] == 11);
    try expect(grid[1][1] == 14);
}

test "one flat array, computed indices" {
    const width = 4;
    const height = 3;
    var cells: [width * height]u8 = @splat(0);

    // The type no longer carries the shape, so the stride is yours to keep
    // right. Worth it when the dimensions are not both compile-time constants.
    cells[1 * width + 2] = 7;

    try expect(cells[6] == 7);
    try expect(cells.len == 12);
}

test "build a lookup table at comptime" {
    // Declare undefined storage, fill it in a loop, break the finished array
    // out of the block. The result is a constant baked into the binary.
    const hex_digits = comptime blk: {
        var table: [16]u8 = undefined;
        for (&table, 0..) |*slot, i| {
            slot.* = if (i < 10) '0' + @as(u8, i) else 'a' + @as(u8, i - 10);
        }
        break :blk table;
    };

    try expect(hex_digits[0] == '0');
    try expect(hex_digits[10] == 'a');
    try expect(hex_digits[15] == 'f');
}

test "the table really is a compile-time constant" {
    const sizes = comptime blk: {
        var table: [4]usize = undefined;
        for (&table, 0..) |*slot, i| slot.* = i * 2;
        break :blk table;
    };

    // An array length has to be known at compile time, so this only compiles
    // if the value came out of comptime. That is the proof, not the claim.
    const buf: [sizes[3]]u8 = @splat(0);
    try expect(buf.len == 6);
}

test "turn a slice back into an array" {
    const numbers = [_]u8{ 1, 2, 3, 4, 5 };
    const slice: []const u8 = &numbers;

    // .* on a slice of comptime-known length copies out a real array, which
    // puts the length back in the type.
    const first_three = slice[0..3].*;
    try expect(@TypeOf(first_three) == [3]u8);
    try expect(first_three.len == 3);

    // It is a copy, so writing to it leaves the original alone.
    var copy = first_three;
    copy[0] = 99;
    try expect(numbers[0] == 1);
}

A grid is an array of arrays

[3][3]u8 is nine bytes in one contiguous block. Both lengths are part of the type, grid.len is the row count and grid[0].len is the column count, and @sizeOf proves there is no pointer hiding anywhere. Index rows first, then columns.

This is not the same type as [][]u8, a slice of slices, which is a list of pointers to rows that may live anywhere. The nested array is one allocation-free block; the slice of slices is however many the code that built it made.

The outer loop hands you a copy

Arrays are values, which the Arrays chapter states. In a nested loop it stops being an abstract rule: for (grid) |row| copies each row, so writing through row changes nothing at all and the compiler has no reason to complain.

for (&grid) |*row| iterates pointers, and then for (row) |*cell| reaches the elements. Both levels have to be pointers before a write lands.

Or one flat array

[width * height]T with y * width + x is the other shape. The type stops carrying the geometry, so the stride becomes yours to keep right, and in exchange the dimensions no longer have to be two separate compile-time constants.

Pick the nested array when the shape is fixed and small. Pick the flat one when the width is computed, when you want to hand the whole thing to something that takes a []T, or when the rows are large enough that copying one by accident would matter.

Tables built at compile time

Declare undefined storage, fill it in a loop, and break :blk the finished array out of the block. Run that inside comptime and the result is a constant baked into the binary, with no initialization code at startup.

The claim is checkable: use one of the table’s entries as an array length. Array lengths must be known at compile time, so if it compiles, the value came out of comptime. That is a proof rather than a promise.

From slice back to array

slice[0..N].* copies out a real [N]T, where N has to be known at compile time. The length moves back into the type, which is what a function expecting *const [4]u8 wants.

It is a copy. Writing to the result leaves the original slice untouched.

Concatenating at runtime is a std.mem.concat away and lives in String Recipes; the operations that work on slices in place are in Slice Recipes.