Skip to content

Why StringHashMap Exists

A StringHashMap is a hash map where the key is a string.

StringHashMap

A StringHashMap is a hash map where the key is a string.

In Zig, a string is usually a slice of bytes:

[]const u8

So this:

std.StringHashMap(u32)

means:

string -> u32

Examples:

"alice" -> 120
"bob" -> 95
"charlie" -> 180

Use StringHashMap when you want to look up values by text.

Why StringHashMap Exists

You already saw AutoHashMap.

For number keys, this is fine:

std.AutoHashMap(u32, []const u8)

But strings need special handling.

A string key is not a single value. It is a slice:

[]const u8

A slice contains:

pointer + length

To compare two strings, Zig must compare their bytes.

These two strings should be considered equal:

const a = "zig";
const b = "zig";

Even if they live at different addresses, their bytes are the same:

z i g
z i g

StringHashMap handles this correctly.

It hashes and compares string contents, not just pointer addresses.

Creating a StringHashMap

Here is a complete empty map:

const std = @import("std");

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();

    const allocator = gpa.allocator();

    var scores = std.StringHashMap(u32).init(allocator);
    defer scores.deinit();
}

This creates a map from strings to unsigned integers.

key type   = []const u8
value type = u32

The allocator is needed because the hash map stores an internal table on the heap.

Adding Values

Use put:

try scores.put("alice", 120);
try scores.put("bob", 95);
try scores.put("charlie", 180);

Full example:

const std = @import("std");

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();

    const allocator = gpa.allocator();

    var scores = std.StringHashMap(u32).init(allocator);
    defer scores.deinit();

    try scores.put("alice", 120);
    try scores.put("bob", 95);
    try scores.put("charlie", 180);
}

put uses try because inserting may allocate memory.

Allocation can fail, so the error must be handled.

Looking Up a Value

Use get:

const score = scores.get("alice");

The result is optional:

?u32

The key might not exist.

A safe lookup looks like this:

if (scores.get("alice")) |score| {
    std.debug.print("alice = {}\n", .{score});
} else {
    std.debug.print("alice not found\n", .{});
}

Output:

alice = 120

If the key does not exist:

if (scores.get("david")) |score| {
    std.debug.print("david = {}\n", .{score});
} else {
    std.debug.print("david not found\n", .{});
}

Output:

david not found

Updating a Value

Calling put with the same key replaces the old value.

try scores.put("alice", 120);
try scores.put("alice", 150);

Now:

"alice" -> 150

For simple values like integers, this is easy.

For values that own heap memory, you must free the old value yourself before replacing it, or use a pattern that lets you inspect the old value.

Counting Words

A common use for StringHashMap is counting words.

Input:

const words = [_][]const u8{
    "zig",
    "c",
    "zig",
    "rust",
    "zig",
    "c",
};

Expected result:

zig  -> 3
c    -> 2
rust -> 1

One simple version:

const std = @import("std");

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();

    const allocator = gpa.allocator();

    const words = [_][]const u8{
        "zig",
        "c",
        "zig",
        "rust",
        "zig",
        "c",
    };

    var counts = std.StringHashMap(u32).init(allocator);
    defer counts.deinit();

    for (words) |word| {
        const old_count = counts.get(word) orelse 0;
        try counts.put(word, old_count + 1);
    }

    var it = counts.iterator();
    while (it.next()) |entry| {
        std.debug.print("{s}: {}\n", .{
            entry.key_ptr.*,
            entry.value_ptr.*,
        });
    }
}

The key line is:

const old_count = counts.get(word) orelse 0;

This means:

use the old count if the word exists
otherwise use 0

Then this stores the new count:

try counts.put(word, old_count + 1);

Counting with getOrPut

A more efficient pattern uses getOrPut.

const result = try counts.getOrPut(word);

This does two jobs:

  1. look for the key
  2. insert it if missing

Then we initialize the value only when the key is new:

if (!result.found_existing) {
    result.value_ptr.* = 0;
}

result.value_ptr.* += 1;

Full loop:

for (words) |word| {
    const result = try counts.getOrPut(word);

    if (!result.found_existing) {
        result.value_ptr.* = 0;
    }

    result.value_ptr.* += 1;
}

This is a common Zig pattern.

The value pointer points into the map. Writing through it changes the stored value directly.

Iterating Over StringHashMap

Use an iterator:

var it = scores.iterator();

while (it.next()) |entry| {
    std.debug.print("{s}: {}\n", .{
        entry.key_ptr.*,
        entry.value_ptr.*,
    });
}

Each entry gives pointers:

entry.key_ptr
entry.value_ptr

To read the stored values, use .*:

entry.key_ptr.*
entry.value_ptr.*

For a string key, print with {s}:

std.debug.print("{s}\n", .{entry.key_ptr.*});

For a number value, print with {}:

std.debug.print("{}\n", .{entry.value_ptr.*});

Iteration Order Is Not Sorted

A StringHashMap does not remember insertion order.

If you insert:

alice
bob
charlie

The iterator may return:

bob
charlie
alice

or another order.

Do not depend on hash map iteration order.

If you need sorted output, collect the keys into an array and sort them.

If you need insertion order, keep a separate ArrayList of keys.

Removing Keys

Use remove:

const removed = scores.remove("bob");

The result is a boolean:

true  = key existed and was removed
false = key was not found

Example:

if (scores.remove("bob")) {
    std.debug.print("removed bob\n", .{});
} else {
    std.debug.print("bob not found\n", .{});
}

Contains

Use contains when you only need to know whether the key exists:

if (scores.contains("alice")) {
    std.debug.print("alice exists\n", .{});
}

Use get when you need the value.

String Literals as Keys

This is safe:

try scores.put("alice", 120);

String literals live for the whole program.

The map stores the slice:

"alice"

It does not need to copy the bytes.

This is fine for static keys like command names, keywords, fixed labels, and test data.

Temporary Strings as Keys

Be careful with temporary strings.

This is dangerous:

var buffer: [32]u8 = undefined;
const name = try std.fmt.bufPrint(&buffer, "user-{d}", .{1});

try scores.put(name, 120);

The key points into buffer.

That is okay only while buffer remains valid and unchanged.

If you later reuse the buffer, the map key may silently change.

Example:

const a = try std.fmt.bufPrint(&buffer, "alice", .{});
try scores.put(a, 120);

const b = try std.fmt.bufPrint(&buffer, "bob", .{});
try scores.put(b, 95);

Both keys may refer to the same buffer memory.

That is not what you want.

Owning String Keys

When keys are created dynamically, usually you should copy them into heap memory.

Use allocator.dupe:

const owned_name = try allocator.dupe(u8, name);
try scores.put(owned_name, 120);

Now the map stores a key whose bytes live on the heap.

But this creates a new responsibility.

You must free that memory later.

Freeing Owned Keys

deinit() frees the map’s internal hash table.

It does not automatically free heap memory used by your keys.

If the map owns its keys, free them before calling deinit():

var it = scores.iterator();
while (it.next()) |entry| {
    allocator.free(entry.key_ptr.*);
}

scores.deinit();

In that case, do not also use:

defer scores.deinit();

unless you make sure the owned keys are freed before it runs.

A clean pattern is:

defer {
    var it = scores.iterator();
    while (it.next()) |entry| {
        allocator.free(entry.key_ptr.*);
    }
    scores.deinit();
}

This puts the cleanup logic in one place.

Owning String Values

The same idea applies to values.

Example:

var users = std.StringHashMap([]u8).init(allocator);

Here the values are mutable byte slices. They may point to heap memory.

If the map owns the values, you must free them:

defer {
    var it = users.iterator();
    while (it.next()) |entry| {
        allocator.free(entry.value_ptr.*);
    }
    users.deinit();
}

If both keys and values are owned, free both:

defer {
    var it = users.iterator();
    while (it.next()) |entry| {
        allocator.free(entry.key_ptr.*);
        allocator.free(entry.value_ptr.*);
    }
    users.deinit();
}

This is normal Zig. Ownership is explicit.

Replacing Owned Values

Be careful with this:

try users.put("alice", try allocator.dupe(u8, "first"));
try users.put("alice", try allocator.dupe(u8, "second"));

The second put replaces the first value.

If you do not free the first value, it leaks.

A safer pattern is to check first:

if (users.get("alice")) |old_value| {
    allocator.free(old_value);
}

try users.put("alice", try allocator.dupe(u8, "second"));

This frees the old value before storing the new one.

Pointers Into the Map

getOrPut gives you pointers into the map.

const result = try scores.getOrPut("alice");
const value_ptr = result.value_ptr;

Use these pointers immediately.

Do not keep them across operations that may grow the map:

const result = try scores.getOrPut("alice");
const value_ptr = result.value_ptr;

try scores.put("bob", 95);

value_ptr.* = 999; // dangerous

The put may resize the map. If the map resizes, old entry pointers may become invalid.

StringHashMap vs StringArrayHashMap

Zig also has ordered map variants, such as std.StringArrayHashMap.

The basic difference:

TypeKeeps insertion order?Lookup by string?
std.StringHashMap(V)NoYes
std.StringArrayHashMap(V)YesYes

Use StringHashMap when lookup speed matters and order does not matter.

Use StringArrayHashMap when you need stable iteration order.

Practical Example: Environment-Like Table

Suppose you want to store configuration values:

host -> localhost
port -> 8080
mode -> debug

You can write:

const std = @import("std");

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();

    const allocator = gpa.allocator();

    var config = std.StringHashMap([]const u8).init(allocator);
    defer config.deinit();

    try config.put("host", "localhost");
    try config.put("port", "8080");
    try config.put("mode", "debug");

    if (config.get("port")) |port| {
        std.debug.print("port = {s}\n", .{port});
    }
}

Output:

port = 8080

This is a natural use of StringHashMap: small string keys and simple values.

Practical Example: Keyword Table

Compilers and parsers often use string maps.

Example:

const TokenKind = enum {
    keyword_fn,
    keyword_var,
    keyword_const,
    identifier,
};

var keywords = std.StringHashMap(TokenKind).init(allocator);
defer keywords.deinit();

try keywords.put("fn", .keyword_fn);
try keywords.put("var", .keyword_var);
try keywords.put("const", .keyword_const);

Then, when reading an identifier:

const kind = keywords.get(text) orelse .identifier;

This means:

if text is a keyword, use its keyword token kind
otherwise, treat it as an identifier

This pattern is common in lexers.

Common Beginner Mistakes

The first mistake is using AutoHashMap([]const u8, V) when you really want string-content comparison. Use StringHashMap(V) for string keys.

The second mistake is storing keys that point into temporary buffers.

The third mistake is forgetting to free owned keys or owned values.

The fourth mistake is depending on iteration order.

The fifth mistake is keeping entry.value_ptr or entry.key_ptr after inserting more items into the map.

A Useful Mental Model

A StringHashMap is a lookup table from text to data.

text -> value

Examples:

"alice" -> User
"main.zig" -> FileInfo
"while" -> TokenKind.keyword_while
"Content-Type" -> "application/json"

The map compares strings by their bytes.

The map owns its internal table.

Your program decides who owns the string data used as keys and values.

Summary

Use std.StringHashMap(V) when your keys are strings.

It is one of the most common containers in Zig programs.

The main rules are:

Use string literals freely when they live for the whole program.

Copy temporary strings into owned memory before storing them.

Free owned keys and values yourself.

Do not depend on iteration order.

Do not keep pointers into the map across inserts or other operations that may resize it.