Slices in Detail

A slice is a view into a sequence of values.

Slices in Detail

A slice is a view into a sequence of values.

It does not own the values. It points to values stored somewhere else.

const values = [_]i32{ 10, 20, 30, 40 };

const part = values[1..3];

The slice part refers to this part of the array:

20, 30

It does not copy those values. It only describes where they are and how many there are.

What a Slice Contains

A slice contains two pieces of information:

pointer to the first element
length

So this type:

[]const i32

means:

a slice of constant i32 values

This type:

[]i32

means:

a slice of mutable i32 values

The difference matters.

A mutable slice lets you change the values through the slice. A constant slice lets you read the values, but not change them.

Creating a Slice from an Array

Use range syntax:

const values = [_]i32{ 10, 20, 30, 40 };

const all = values[0..];
const first_two = values[0..2];
const middle = values[1..3];

The ranges mean:

Expression Values
values[0..] 10, 20, 30, 40
values[0..2] 10, 20
values[1..3] 20, 30

The start index is included. The end index is excluded.

So:

values[1..3]

means:

start at index 1
stop before index 3

That gives indexes 1 and 2.

Slice Length

A slice has a .len field.

const values = [_]i32{ 10, 20, 30, 40 };
const part = values[1..3];

const n = part.len;

Here, part.len is 2.

You can loop over a slice the same way you loop over an array:

const std = @import("std");

pub fn main() void {
    const values = [_]i32{ 10, 20, 30, 40 };
    const part = values[1..3];

    for (part) |value| {
        std.debug.print("{}\n", .{value});
    }
}

Output:

20
30

Slices Do Not Own Memory

This is the most important rule.

A slice refers to memory owned by something else.

var values = [_]i32{ 10, 20, 30, 40 };
var part = values[1..3];

part[0] = 99;

Now values contains:

10, 99, 30, 40

The slice did not contain a separate copy. It pointed into the original array.

This is why slices are useful. You can pass part of an array to a function without copying it.

Passing Slices to Functions

A function that accepts a slice can work with many lengths.

fn sum(values: []const i32) i32 {
    var total: i32 = 0;

    for (values) |value| {
        total += value;
    }

    return total;
}

This function accepts any number of i32 values.

const a = [_]i32{ 1, 2, 3 };
const b = [_]i32{ 10, 20, 30, 40, 50 };

const x = sum(a[0..]);
const y = sum(b[1..4]);

The first call passes all of a.

The second call passes:

20, 30, 40

This is more flexible than a function that accepts a fixed array:

fn sumThree(values: [3]i32) i32 {
    return values[0] + values[1] + values[2];
}

sumThree accepts exactly 3 values. sum accepts any length.

Constant Slices

A constant slice prevents mutation through the slice.

fn printAll(values: []const i32) void {
    for (values) |value| {
        std.debug.print("{}\n", .{value});
    }
}

The function can read values, but it cannot do this:

values[0] = 99;

Use []const T when a function only needs to read the data.

This is a good default for function parameters.

fn contains(values: []const i32, target: i32) bool {
    for (values) |value| {
        if (value == target) return true;
    }

    return false;
}

The function does not need to modify the slice, so []const i32 is the right type.

Mutable Slices

Use []T when a function needs to modify values.

fn fill(values: []i32, replacement: i32) void {
    for (values) |*value| {
        value.* = replacement;
    }
}

Usage:

var values = [_]i32{ 1, 2, 3, 4 };

fill(values[0..], 0);

Now the array contains:

0, 0, 0, 0

The loop uses:

|*value|

This captures each element by pointer, so the function can write to it.

Then:

value.* = replacement;

writes into the element.

Slicing a Slice

You can create a smaller slice from an existing slice.

const values = [_]i32{ 10, 20, 30, 40, 50 };

const a = values[0..];
const b = a[1..4];
const c = b[1..];

The values are:

Name Values
a 10, 20, 30, 40, 50
b 20, 30, 40
c 30, 40

All of these slices refer to the same original array.

No values are copied.

Bounds Checking

Zig checks slice indexes in safe build modes.

const values = [_]i32{ 10, 20, 30 };

const bad = values[1..5];

This is invalid because index 5 is past the end of the array.

For a slice of length 3, valid indexes are:

0, 1, 2

For slicing ranges, the end may equal the length.

const ok = values[1..3];

This is valid because it stops before index 3.

But this is invalid:

const bad = values[1..4];

Index 4 is beyond the end.

Empty Slices

A slice can be empty.

const values = [_]i32{ 10, 20, 30 };

const empty = values[1..1];

The start and end are the same, so the slice has length zero.

empty.len == 0

An empty slice is valid. It simply has no elements.

This is useful because many functions can handle empty input naturally.

fn sum(values: []const i32) i32 {
    var total: i32 = 0;

    for (values) |value| {
        total += value;
    }

    return total;
}

If values is empty, the loop runs zero times and the result is 0.

Slice Syntax Review

Here are the common forms:

Syntax Meaning
array[0..] Slice from index 0 to the end
array[start..end] Slice from start up to but not including end
slice[start..end] Smaller slice from an existing slice
array[index] One element, not a slice

Do not confuse these:

values[1]

This gives one value.

values[1..2]

This gives a slice containing one value.

The types are different.

If values is an array of i32, then:

values[1]

has type:

i32

But:

values[1..2]

has type:

[]const i32

or:

[]i32

depending on whether the original data is constant or mutable.

Slices and Strings

In Zig, strings are commonly represented as slices of bytes.

[]const u8

means:

read-only sequence of bytes

Many functions that accept text use []const u8.

Example:

const std = @import("std");

fn greet(name: []const u8) void {
    std.debug.print("Hello, {s}!\n", .{name});
}

pub fn main() void {
    greet("Zig");
}

Output:

Hello, Zig!

The formatting marker {s} prints a byte slice as a string.

A string literal can be passed where []const u8 is expected.

Slices and Lifetime

A slice must not outlive the memory it points to.

This is invalid in spirit:

fn bad() []const i32 {
    const values = [_]i32{ 1, 2, 3 };
    return values[0..];
}

The array values lives inside the function. When the function returns, that local array is gone. Returning a slice to it would leave the caller with a pointer to invalid memory.

Zig tries to catch many lifetime mistakes, but you should learn the rule early:

A slice is only valid while the original storage is valid.

Good examples of valid storage:

global constant data
caller-owned arrays
heap allocations that are still alive
buffers that remain in scope

Bad examples:

local arrays that disappear after return
temporary buffers that get freed
memory owned by an allocator after deallocation

Slices and Allocation

A slice itself does not allocate memory.

This function does not allocate:

fn firstHalf(values: []const i32) []const i32 {
    return values[0 .. values.len / 2];
}

It returns a view into the original slice.

This is cheap. It only creates another pointer and length pair.

Allocation happens only when you explicitly ask an allocator for memory.

const buffer = try allocator.alloc(u8, 1024);

That returns a slice of newly allocated memory:

[]u8

The allocator owns the memory until you free it.

defer allocator.free(buffer);

Now the slice buffer points to heap memory.

Common Mistake: Returning a Slice to Local Data

Do not do this:

fn makeName() []const u8 {
    const name = [_]u8{ 'Z', 'i', 'g' };
    return name[0..];
}

The array disappears when the function returns.

Instead, use caller-provided memory, allocated memory, or constant data.

Constant data example:

fn name() []const u8 {
    return "Zig";
}

Caller-provided buffer example:

fn writeName(buffer: []u8) []u8 {
    buffer[0] = 'Z';
    buffer[1] = 'i';
    buffer[2] = 'g';
    return buffer[0..3];
}

Common Mistake: Expecting a Copy

This code modifies the original array:

var values = [_]i32{ 1, 2, 3, 4 };
var part = values[1..3];

part[0] = 99;

Afterward:

values = 1, 99, 3, 4

A slice is a view, not a copy.

If you need a separate copy, allocate or create another array and copy the data.

Common Mistake: Using Mutable Slices When Read-Only Is Enough

This function should use []const i32:

fn printAll(values: []i32) void {
    for (values) |value| {
        std.debug.print("{}\n", .{value});
    }
}

It does not modify the slice, so write:

fn printAll(values: []const i32) void {
    for (values) |value| {
        std.debug.print("{}\n", .{value});
    }
}

This makes the function easier to call and safer to use.

A Complete Example

const std = @import("std");

fn sum(values: []const i32) i32 {
    var total: i32 = 0;

    for (values) |value| {
        total += value;
    }

    return total;
}

fn fill(values: []i32, replacement: i32) void {
    for (values) |*value| {
        value.* = replacement;
    }
}

pub fn main() void {
    var numbers = [_]i32{ 10, 20, 30, 40, 50 };

    const middle = numbers[1..4];
    std.debug.print("middle sum = {}\n", .{sum(middle)});

    fill(numbers[0..2], 0);

    for (numbers) |value| {
        std.debug.print("{} ", .{value});
    }
    std.debug.print("\n", .{});
}

Output:

middle sum = 90
0 0 30 40 50

The slice middle refers to 20, 30, 40.

The call:

fill(numbers[0..2], 0);

modifies the first two elements of the original array.

Summary

A slice is a pointer plus a length.

It is written like this:

[]T

or:

[]const T

Use []const T when you only need to read values. Use []T when you need to modify values.

A slice does not own memory. It points to memory owned by an array, an allocation, a string literal, or some other storage.

Slices are one of the most important types in Zig. They let functions work with data of many lengths without copying the data.