Skip to content

Slices in Detail

A slice is a view into a sequence of values.

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:

ExpressionValues
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:

NameValues
a10, 20, 30, 40, 50
b20, 30, 40
c30, 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:

SyntaxMeaning
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.