पाठ 16 / 25

Integer Semantics, Safety Checks and Undefined Behaviour

Understand overflow, wrapping and saturating arithmetic, and what build modes check.

Safety you can see

In Zig, integer overflow is illegal behaviour: in Debug and ReleaseSafe builds it panics with a clear message; in ReleaseFast and ReleaseSmall it is undefined behaviour that the optimiser may exploit. When you want other semantics, say so explicitly: wrapping operators +%, -%, *% wrap around (useful for hashes and counters), saturating operators +|, -|, *| clamp at the limits, and builtins such as @addWithOverflow return the result and an overflow bit. Other safety checks in safe modes: array and slice bounds, unwrapping a null optional with .?, @intCast of an out-of-range value, accessing the inactive field of a tagged union, unreachable being reached, division by zero and misaligned pointer casts. Each can be disabled locally with @setRuntimeSafety(false) in hot code once proven correct. Zig does not prevent all memory errors (use-after-free, data races, dangling pointers), unlike Rust; it offers good defaults, debugging allocators, and explicitness. A common strategy is to ship ReleaseSafe for most software and use ReleaseFast only where measurements justify it.

Build modes and safety

Debug and ReleaseSafe keep runtime checks; ReleaseFast and ReleaseSmall remove them.

A two-by-two grid of tiles labelled by build mode, two with shield icons and two with speedometer icons.
Figure 6.1 — Safety versus speed by build mode.

Overflow, wrapping and saturating arithmetic

Explicit choices instead of silent wrap-around.

const std = @import("std");

pub fn main() void {
    var hits: u8 = 250;
    hits +%= 10;                           // wrapping: 260 mod 256 = 4
    std.debug.print("wrapped: {d}\n", .{hits});

    var stock: u8 = 5;
    stock -|= 8;                           // saturating: clamps at 0 instead of underflowing
    std.debug.print("saturated: {d}\n", .{stock});

    const big: u32 = 4_000_000_000;
    const result = @addWithOverflow(big, big);   // returns .{ wrapped_result, overflow_bit }
    if (result[1] == 1) {
        std.debug.print("overflow detected, wrapped value {d}\n", .{result[0]});
    }

    const percent: u8 = 120;
    const capped: u8 = @min(percent, 100);
    std.debug.print("capped: {d}\n", .{capped});

    // var x: u8 = 255;
    // x += 1;           // Debug/ReleaseSafe: panic "integer overflow"; ReleaseFast: undefined behaviour
}

fn hashBytes(data: []const u8) u32 {       // FNV-1a: wrapping multiplication is intended
    var h: u32 = 2166136261;
    for (data) |b| {
        h ^= b;
        h *%= 16777619;
    }
    return h;
}

Ship ReleaseSafe by default

The cost of bounds and overflow checks is usually small. Keeping them in production turns silent memory corruption into a clear crash with a stack trace, which is much easier to diagnose.

त्वरित जाँच: What does `x +|= 10` do for an unsigned integer near its maximum?

  • Wraps around to a small number
  • Panics
  • Saturates at the maximum value of the type
  • Converts to a larger type
Answer

Saturates at the maximum value of the type — Saturating operators clamp to the type's range.