पाठ 24 / 25

Idiomatic Zig and Common Pitfalls

Write clear Zig and avoid frequent mistakes.

Writing Zig the Zig way

Idiomatic Zig is explicit and boring in the best sense. Pass allocators to anything that allocates, and document ownership ("caller owns the returned memory"). Pair every acquisition with defer or errdefer on the next line. Return error unions with meaningful error sets, use try to propagate, and handle errors where you can do something useful. Prefer slices to many-item pointers, tagged unions and exhaustive switch for alternatives, const wherever possible, and comptime for validation and specialisation rather than clever metaprogramming. Run zig fmt and follow the naming conventions: camelCase functions, TitleCase types, snake_case variables and fields. Common pitfalls: dangling pointers to stack variables or into a reallocated ArrayList; storing non-owned string keys in hash maps; forgetting to free (caught by std.testing.allocator) or freeing with the wrong allocator; forgetting flush on buffered writers; relying on undefined behaviour that only appears in ReleaseFast; catch unreachable on errors that can actually happen; using undefined values before setting them; and copying examples written for older Zig versions, whose std APIs may no longer exist.

Pitfalls and their fixes

Dangling pointers, catch unreachable and hash map keys.

const std = @import("std");

// PITFALL: returns a slice of a stack buffer that dies when the function returns
fn badGreeting(name: []const u8) []const u8 {
    var buf: [64]u8 = undefined;
    return std.fmt.bufPrint(&buf, "Hi {s}", .{name}) catch "Hi";   // dangling!
}

// FIX: let the caller provide the buffer (or pass an allocator)
fn greeting(buf: []u8, name: []const u8) ![]const u8 {
    return std.fmt.bufPrint(buf, "Hi {s}", .{name});
}

// PITFALL: catch unreachable on input that can be invalid
fn parsePortBad(text: []const u8) u16 {
    return std.fmt.parseInt(u16, text, 10) catch unreachable;   // panics (or UB in ReleaseFast)
}

// FIX: return the error, or handle it with a meaningful default
fn parsePort(text: []const u8) !u16 {
    return std.fmt.parseInt(u16, text, 10);
}

// PITFALL: hash map keys pointing at a buffer that is reused
fn countWords(gpa: std.mem.Allocator, words: []const []const u8) !std.StringHashMap(u32) {
    var map = std.StringHashMap(u32).init(gpa);
    errdefer map.deinit();
    for (words) |w| {
        const entry = try map.getOrPut(w);          // OK here: words outlive the map
        if (!entry.found_existing) entry.value_ptr.* = 0;
        entry.value_ptr.* += 1;
    }
    return map;   // if keys came from a reused read buffer, dupe them with gpa.dupe(u8, w)
}

test "fixed versions" {
    var buf: [32]u8 = undefined;
    try std.testing.expectEqualStrings("Hi Asha", try greeting(&buf, "Asha"));
    try std.testing.expectError(error.InvalidCharacter, parsePort("80a"));

    var map = try countWords(std.testing.allocator, &.{ "pen", "ink", "pen" });
    defer map.deinit();
    try std.testing.expectEqual(@as(?u32, 2), map.get("pen"));
}

unreachable is a promise

Writing unreachable tells the compiler a path can never happen. In safe modes it panics if you were wrong; in ReleaseFast it is undefined behaviour. Use it only for real invariants, never for input you do not control.

त्वरित जाँच: Why is returning a slice of a local array from a function a bug?

  • The array lives on the function's stack frame, so the slice dangles after return
  • Slices cannot be returned
  • It is slow
  • Arrays are always heap-allocated
Answer

The array lives on the function's stack frame, so the slice dangles after return — Stack memory is reclaimed when the function returns.