An Allocator is the piece that allows a structure to request dynamic memory when it doesn’t know in advance how much it will grow.
So far we’ve seen allocators quite directly: request memory, use it, and free it. But where they truly become everyday tools is in the dynamic structures of the standard library, such as ArrayList and HashMap.
In Zig, many structures do not hide the memory they use. If a collection can grow, there will normally be an allocator nearby. It’s Zig’s way of making it clear that this operation may allocate memory.
Why a Collection Needs Memory
A normal array ([N]T) has a fixed size. The compiler knows how many elements there are and reserves that space.
const numbers = [_]u8{ 10, 20, 30 };That’s fine if we know the size in advance. But often we don’t:
- We read lines from a file.
- We receive data over the network.
- We are accumulating results.
- We build a list of errors, tokens, or entities.
That’s when we need a structure that can grow. And if something can grow, someone has to give it new memory.
That someone is the Allocator.
ArrayList
An ArrayList is a dynamic list. Conceptually, it’s similar to a C++ vector or a list in other languages, but with Zig’s philosophy: memory is not hidden.
const std = @import("std");
pub fn main() !void {
var debug_allocator = std.heap.DebugAllocator(.{}){};
defer _ = debug_allocator.deinit();
const allocator = debug_allocator.allocator();
var list: std.ArrayList(u8) = .empty;
defer list.deinit(allocator);
try list.append(allocator, 10);
try list.append(allocator, 20);
try list.append(allocator, 30);
for (list.items) |value| {
std.debug.print("{d}\n", .{value});
}
}Three important details:
- We create the list with
.empty. - Each operation that can grow receives the
allocator. - We free the memory with
deinit(allocator).
The list exposes its elements through list.items, which is a slice ([]T). This is very convenient because we can iterate over it like any other slice.
If you save list.items and then do more append operations, the list may reallocate memory. In that case, the previous slice becomes invalid. The same applies to pointers obtained from its elements.
Capacity and Reallocations
An ArrayList does not request memory element by element. It normally reserves a block with a certain capacity and fills it.
If it runs out of space, it requests a larger block, copies the data, and frees the previous block. This is called reallocating memory.
If you roughly know how many elements you’ll store, you can reserve capacity before starting:
var list: std.ArrayList(u32) = .empty;
defer list.deinit(allocator);
try list.ensureTotalCapacity(allocator, 1000);
for (0..1000) |i| {
try list.append(allocator, @intCast(i));
}This avoids many intermediate reallocations. It’s not always necessary, but in performance-sensitive code it can make a difference.
HashMap
A HashMap is a key-value table. That is, a structure where we store a value associated with a key.
For example:
“temperature” -> 23 “humidity” -> 61 “pressure” -> 1013
In Zig, one of the most common variants for text keys is std.StringHashMap.
const std = @import("std");
pub fn main() !void {
var debug_allocator = std.heap.DebugAllocator(.{}){};
defer _ = debug_allocator.deinit();
const allocator = debug_allocator.allocator();
var sensors = std.StringHashMap(u32).init(allocator);
defer sensors.deinit();
try sensors.put("temperature", 23);
try sensors.put("humidity", 61);
try sensors.put("pressure", 1013);
if (sensors.get("humidity")) |value| {
std.debug.print("Humidity: {d}%\n", .{value});
}
}Here too there is dynamic memory. The map needs to reserve space for its internal buckets, and it can grow as we insert more elements.
Iterating Over a HashMap
To iterate over a HashMap, we use an iterator.
var it = sensors.iterator();
while (it.next()) |entry| {
std.debug.print("{s}: {d}\n", .{ entry.key_ptr.*, entry.value_ptr.* });
}Each entry gives us pointers to the key and value. That’s why we access them with .*.
It may seem a bit more explicit than in other languages, but it’s consistent with Zig: if you’re touching indirect memory, it shows in the code.
ArrayList vs HashMap
Although both structures use allocators, they serve different purposes.
| Structure | Typical Use | Access |
|---|---|---|
ArrayList(T) | Ordered list of elements | By index |
StringHashMap(T) | Dictionary with text keys | By key |
AutoHashMap(K, V) | Dictionary for keys compatible with autoHash, like integers or enums. | By key |
If you just need to accumulate elements, use ArrayList. If you need to look up by a key, use a HashMap.
StringHashMap does not copy the keys when inserting them. The text used as a key must remain alive as long as it stays inside the map. The literals in the example last for the entire program; for temporary text, you’ll need to duplicate it and free that copy when appropriate.