Zig Slices

A slice is a window into an array. It knows where a portion of memory starts and how many elements it contains, without owning that memory itself. Slices let you write functions that work with any portion of any array — regardless of the array's full size.

The Slice Model

  Full array:
  Index: [ 0 | 1 | 2 | 3 | 4 | 5 | 6 ]
  Value: [10 |20 |30 |40 |50 |60 |70 ]

  Slice: arr[2..5]
              ↑              ↑
           start           end (exclusive)
  Sees:  [30 | 40 | 50 ]   (indices 2, 3, 4)
  A slice internally stores:
  +------------------+
  | pointer → arr[2] |   Points to element at index 2
  | length = 3       |   Covers 3 elements
  +------------------+

Creating a Slice

const arr = [_]u32{ 10, 20, 30, 40, 50 };
const slc: []const u32 = arr[1..4];

for (slc) |val| {
    std.debug.print("{d}\n", .{val});
}
// Output: 20, 30, 40

The range 1..4 starts at index 1 and ends before index 4. Index 4 is not included. The slice slc sees exactly three elements: those at positions 1, 2, and 3 in the original array.

Slice Syntax Variants

  arr[2..5]   → elements at index 2, 3, 4
  arr[2..]    → from index 2 to end of array
  arr[..5]    → from start to index 4 (5 exclusive)
  arr[0..arr.len] → entire array as a slice (same as arr[..])

Mutable Slices

A slice of a var array can modify the original array's elements through the slice:

var data = [_]i32{ 1, 2, 3, 4, 5 };
const mid: []i32 = data[1..4];

mid[0] = 99;  // Modifies data[1]
mid[2] = 77;  // Modifies data[3]

std.debug.print("{d}\n", .{data});
// Output: { 1, 99, 3, 77, 5 }
  data: [ 1 | 2 | 3 | 4 | 5 ]
                ↑       ↑
         mid[0]=99  mid[2]=77

  After modification:
  data: [ 1 | 99 | 3 | 77 | 5 ]

The slice does not copy the data — it points to the same memory. Changing elements through the slice changes the original array. This is intentional and efficient: no copying happens.

Read-Only Slices

Use []const u32 when you want to read but not modify:

fn printAll(items: []const u32) void {
    for (items) |item| {
        std.debug.print("{d} ", .{item});
    }
    std.debug.print("\n", .{});
}

The const in the slice type is a promise: this function will not change what the slice points to. The compiler enforces this — any attempt to write through a []const slice fails at compile time.

String as a Slice

In Zig, strings are slices of bytes — specifically []const u8. A string literal is a slice pointing to read-only memory:

const greeting: []const u8 = "Hello, World!";
std.debug.print("Length: {d}\n", .{greeting.len});    // 13
std.debug.print("First char: {c}\n", .{greeting[0]}); // H
  "Hello, World!"
  [ H | e | l | l | o | , |   | W | o | r | l | d | ! ]
    0   1   2   3   4   5   6   7   8   9  10  11  12

  greeting.len = 13
  greeting[0]  = 'H' (72 in ASCII)

Working with strings in Zig is working with byte slices. The standard library provides tools in std.mem for searching, comparing, and splitting string slices.

Slice of a Slice

You can slice a slice to get a narrower view:

const sentence: []const u8 = "The quick brown fox";
const word: []const u8 = sentence[4..9];
std.debug.print("{s}\n", .{word});  // quick
  "The quick brown fox"
    0123456789...

  sentence[4..9]:
  index 4 = 'q'
  index 8 = 'k' (last included)
  → "quick"

Sentinel-Terminated Slices

C strings end with a zero byte. Zig represents these with sentinel-terminated slices:

  Type:   [:0]const u8
  Meaning: Slice of bytes where a zero byte follows the last element.

  const c_str: [:0]const u8 = "hello";
  // Memory: ['h','e','l','l','o', 0]
  //                                 ↑ sentinel (zero byte)

This type is essential when passing strings to C libraries that expect null-terminated strings. Zig's type system tracks whether a slice is sentinel-terminated, preventing you from accidentally passing a plain slice where a C string is required.

Slice Bounds Checking

  const arr = [_]u32{ 1, 2, 3 };
  const bad = arr[0..10];  // arr has only 3 elements!
  // → Compile error (if size known at compile time)
  //   or Runtime panic (if size only known at runtime)

Zig checks slice bounds in debug builds. Attempting to create a slice that goes beyond the array's end causes an immediate panic with a clear message. Release builds skip these checks for performance — your responsibility is to verify bounds are correct before building in release mode.

Common Standard Library Slice Functions

  std.mem.eql(u8, a, b)         → compare two slices for equality
  std.mem.startsWith(u8, s, p)  → does s start with prefix p?
  std.mem.endsWith(u8, s, s2)   → does s end with suffix s2?
  std.mem.indexOf(u8, hay, ndl) → find needle in haystack slice
  std.mem.copy(u8, dest, src)   → copy bytes from src to dest

Practical Example: Finding a Word in Text

const std = @import("std");

pub fn main() void {
    const text: []const u8 = "Zig is fast and safe";
    const word: []const u8 = "fast";

    if (std.mem.indexOf(u8, text, word)) |pos| {
        std.debug.print("Found '{s}' at position {d}\n", .{word, pos});
    } else {
        std.debug.print("'{s}' not found.\n", .{word});
    }
}

Output:

Found 'fast' at position 11

std.mem.indexOf returns an optional usize — the position if found, or null if not. The if-capture pattern if (...) |pos| unwraps the position only when it exists. If the word is absent, the else branch runs. This single pattern replaces the need for special sentinel return values like -1 used in C.

Leave a Comment

Your email address will not be published. Required fields are marked *