Skip to content

The Allocator Interface

An allocator is a value that knows how to allocate and free memory.

An allocator is a value that knows how to allocate and free memory.

In Zig, allocator-using code normally receives an allocator as an argument:

fn readFile(allocator: std.mem.Allocator, path: []const u8) ![]u8

This function does not choose the allocation policy. The caller chooses it.

That makes the function more general. It can be used with a page allocator, an arena allocator, a fixed-buffer allocator, or a testing allocator.

The type is:

std.mem.Allocator

It is an interface-like value. Code that receives it can ask for memory without knowing the concrete allocator behind it.

The common operation is alloc:

const items = try allocator.alloc(i32, 10);
defer allocator.free(items);

This requests memory for ten i32 values.

The result is a slice:

[]i32

The slice has a pointer and a length. It does not own memory by itself. Ownership is a rule of the program.

The matching operation is free:

allocator.free(items);

The same allocator that allocated the memory must free it.

This is important. Memory allocated by one allocator must not be freed by another.

A small example:

const std = @import("std");

pub fn main() !void {
    const allocator = std.heap.page_allocator;

    const bytes = try allocator.alloc(u8, 1024);
    defer allocator.free(bytes);

    @memset(bytes, 0);

    std.debug.print("allocated {d} bytes\n", .{bytes.len});
}

Allocation can fail, so alloc is used with try.

Freeing memory usually does not fail.

The allocator also supports creating one value:

const p = try allocator.create(i32);
defer allocator.destroy(p);

p.* = 42;

create(T) allocates space for one T and returns *T.

destroy(p) frees memory allocated by create.

Use alloc and free for slices.

Use create and destroy for single values.

const xs = try allocator.alloc(i32, 8);
defer allocator.free(xs);

const x = try allocator.create(i32);
defer allocator.destroy(x);

These pairs should not be mixed.

Do not do this:

const x = try allocator.create(i32);
defer allocator.free(x); // wrong

And do not do this:

const xs = try allocator.alloc(i32, 8);
defer allocator.destroy(xs); // wrong

The names are part of the discipline.

Allocation also appears inside standard-library containers. For example, an ArrayList stores elements in memory that may grow.

const std = @import("std");

pub fn main() !void {
    var list = std.ArrayList(i32).init(std.heap.page_allocator);
    defer list.deinit();

    try list.append(10);
    try list.append(20);

    std.debug.print("{any}\n", .{list.items});
}

The list receives an allocator during initialization.

When the list grows, it uses that allocator.

When deinit runs, the list frees its internal memory.

The allocator is still visible. It is not global. It is not hidden by the language.

The allocator interface gives Zig a simple pattern:

fn work(allocator: std.mem.Allocator) !void {
    const memory = try allocator.alloc(u8, 4096);
    defer allocator.free(memory);

    // use memory
}

The caller controls allocation.

The callee controls use.

Ownership must be clear between them.

Exercises:

Exercise 12-1. Allocate a slice of ten u64 values and fill it with the numbers 0 through 9.

Exercise 12-2. Allocate one i32 with create, store 123 in it, print it, and destroy it.

Exercise 12-3. Write a function that takes an allocator and returns an allocated copy of the string "zig".

Exercise 12-4. Modify the ArrayList example to append five numbers and print their sum.