Beginner

Operators in Zig

Explore arithmetic, comparison, logical, bitwise, and Zig-specific wrapping and saturating operators with runnable Docker examples

Operators are the verbs of a programming language — they describe what to do with the values your variables hold. Because Zig is a systems language with a “no hidden control flow” philosophy, its operators behave exactly as written: there is no operator overloading, no implicit numeric conversions, and no hidden allocations behind an expression.

Zig also exposes operators that most high-level languages hide: wrapping arithmetic for when you actually want modular overflow, saturating arithmetic for when you want to clamp at a limit, and built-in functions like @divTrunc and @rem for cases where the right behavior is ambiguous for signed integers.

This tutorial walks through Zig’s operator categories with a single runnable program so you can see how each one behaves in practice.

Arithmetic, Wrapping, and Saturating Operators

Zig’s standard arithmetic operators (+, -, *, /, %) check for overflow in debug builds. For signed integers, / and % are only allowed when the operands divide evenly — otherwise you must use @divTrunc, @divFloor, or @rem to make your intent explicit.

For situations where you want overflow to wrap around (common in hashing or fixed-width arithmetic), Zig provides the +%, -%, and *% operators. For situations where you want the value to clamp at the type’s minimum or maximum, use the saturating variants +|, -|, and *|.

Create a file named operators.zig:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
const std = @import("std");

pub fn main() !void {
    const stdout = std.io.getStdOut().writer();

    // --- Arithmetic on signed integers ---
    const a: i32 = 17;
    const b: i32 = 5;
    try stdout.print("a + b      = {d}\n", .{a + b});
    try stdout.print("a - b      = {d}\n", .{a - b});
    try stdout.print("a * b      = {d}\n", .{a * b});
    try stdout.print("@divTrunc  = {d}\n", .{@divTrunc(a, b)});
    try stdout.print("@rem       = {d}\n", .{@rem(a, b)});

    // --- Floating-point division ---
    const x: f64 = 17.0;
    const y: f64 = 5.0;
    try stdout.print("x / y      = {d:.2}\n", .{x / y});

    // --- Wrapping and saturating on u8 ---
    const max: u8 = 255;
    const wrapped: u8 = max +% 1;
    const saturated: u8 = max +| 10;
    try stdout.print("255 +% 1   = {d}\n", .{wrapped});
    try stdout.print("255 +| 10  = {d}\n", .{saturated});

    // --- Comparison operators ---
    try stdout.print("a == b     = {}\n", .{a == b});
    try stdout.print("a != b     = {}\n", .{a != b});
    try stdout.print("a >  b     = {}\n", .{a > b});

    // --- Logical operators (short-circuit) ---
    const t = true;
    const f = false;
    try stdout.print("t and f    = {}\n", .{t and f});
    try stdout.print("t or  f    = {}\n", .{t or f});
    try stdout.print("!t         = {}\n", .{!t});

    // --- Bitwise operators ---
    const m: u8 = 0b1100;
    const n: u8 = 0b1010;
    try stdout.print("m & n      = {b}\n", .{m & n});
    try stdout.print("m | n      = {b}\n", .{m | n});
    try stdout.print("m ^ n      = {b}\n", .{m ^ n});
    try stdout.print("m << 1     = {b}\n", .{m << 1});

    // --- Array/string concatenation and repetition (comptime) ---
    const greeting = "Hello, " ++ "Zig!";
    const line = "-" ** 12;
    try stdout.print("{s}\n", .{greeting});
    try stdout.print("{s}\n", .{line});

    // --- Optional unwrap with `orelse` ---
    const maybe_value: ?i32 = 42;
    const missing: ?i32 = null;
    try stdout.print("maybe orelse 0 = {d}\n", .{maybe_value orelse 0});
    try stdout.print("null  orelse 0 = {d}\n", .{missing orelse 0});
}

A few notes on what makes this code distinctly Zig:

  • @divTrunc and @rem are built-in functions, not operators. Zig forces you to pick a rounding mode (@divTrunc, @divFloor, @divExact) for signed integer division so the behavior at negative inputs is never ambiguous.
  • +% and +| are first-class operators in the grammar — overflow handling is part of the language, not a library function.
  • ++ and ** work on arrays and strings at compile time only; the operands must be comptime-known values.
  • orelse is the operator for unwrapping optional types (?T) with a default value.

Running with Docker

1
2
3
4
5
# Pull the Zig toolchain image
docker pull kassany/alpine-ziglang:0.14.0

# Run the operators example
docker run --rm -v $(pwd):/app -w /app kassany/alpine-ziglang:0.14.0 zig run operators.zig

Expected Output

a + b      = 22
a - b      = 12
a * b      = 85
@divTrunc  = 3
@rem       = 2
x / y      = 3.40
255 +% 1   = 0
255 +| 10  = 255
a == b     = false
a != b     = true
a >  b     = true
t and f    = false
t or  f    = true
!t         = false
m & n      = 1000
m | n      = 1110
m ^ n      = 110
m << 1     = 11000
Hello, Zig!
------------
maybe orelse 0 = 42
null  orelse 0 = 0

Operator Precedence

Zig has fewer precedence levels than C and avoids the most error-prone ambiguities. A few rules worth remembering:

  • Unary operators (-, !, ~) bind tighter than any binary operator.
  • Multiplicative operators (*, /, %, **) bind tighter than additive operators (+, -, ++).
  • Bit-shift operators (<<, >>) sit between additive and bitwise tiers.
  • Bitwise operators (&, |, ^) share a single tier along with orelse and catch — lower than shifts but higher than comparisons.
  • Comparison operators (==, !=, <, >, <=, >=) cannot be chained; a < b < c is a compile error.
  • Logical and has higher precedence than or, and both sit below comparisons, which is what you want for expressions like x > 0 and x < 10.

When you mix operators across categories in an ambiguous way, Zig refuses to guess and requires explicit parentheses — for example, a & b == c will not compile without them. This is intentional and aligned with the language’s “no hidden behavior” philosophy.

Key Concepts

  • Explicit overflow semantics — Default +, -, * panic on overflow in debug mode; +%/-%/*% wrap; +|/-|/*| saturate. You always know which behavior you opted into.
  • Signed division is a builtin, not an operator — Use @divTrunc, @divFloor, or @divExact (and @rem / @mod) to spell out the rounding behavior for negative operands.
  • No operator overloadinga + b always means primitive addition. There are no user-defined operators in Zig.
  • ++ and ** are compile-time only — String concatenation and array repetition happen at compile time, with zero runtime cost.
  • orelse is the optional-unwrap operator — It provides a default value for null and works seamlessly with Zig’s ?T optional types.
  • Bitwise operators sit at the same precedence as arithmetic — Unlike C, Zig demands explicit parentheses when mixing bitwise and comparison expressions, eliminating a classic source of bugs.
  • Logical and/or short-circuit — The right-hand operand is not evaluated when the result is already determined, just like in C and most modern languages.

Running Today

All examples can be run using Docker:

docker pull kassany/alpine-ziglang:0.14.0
Last updated:

Comments

Loading comments...

Leave a Comment

2000 characters remaining