Instantiate a slice for tests

fn count(words: [][]u8) u8 {
    return words.len;
}

test "returns number of words" {
    const words: [][]u8 = &.{ "hello", "world" };
    _ = count(words);
}

This code will not run. It produces the following error when I run zig test scratch.zig:

scratch.zig:6:31: error: expected type '[]u8', found '*const [5:0]u8'
    const words: [][]u8 = &.{ "hello", "world!" };
                              ^~~~~~~
scratch.zig:6:31: note: cast discards const qualifier

So first I’m wondering: why can’t I create a slice like this? I know I can make it a const [] const []u8 as detailed in the answer How do I initialize a slice of slices in Zig?, but I think it’s reasonable to non-const slice (or whatever that’s called) sometimes. So second, I’m wondering: Is there a good way to initialize one of those?

The issue is string literals "asdlf" are const pointers, but you are asking for non const slices.

If you change words: [][]const u8, you will then find the same issue with the outer slice as well, since &.{} is a pointer to a literal it is also const.

If you do words: []const []const u8 it will work. Your next issue is your function requires non const slices.

To get non const slices, you need to allocate stable and mutable memory on either the heap or the stack:

test "returns number of words" {
    // "asdf" is a pointer to an array
    // so we can dereference it to copy the array to mutable memory
    var hello = "hello".*;
    var world = "world".*;
    // must be an array to get mutable memory for the outer slice
    var words: [2][]u8 = .{&hello, &world}; 
    _ = count(&words); // coerce the array to a slice
}
2 Likes

I don’t think I explained it well, so I’ll try again.

In zig allocating to any kind of memory is explicit:

  • Allocator is used allocate to the ‘heap’
  • const/var in a function allocates to the stack
  • const/var in a container (file or type) allocates to global memory
  • prefixing var (const is not allowed) with threadlocal allocates to thread memory.

There is a caveat that in any context const may instead allocate to global read only memory if its value is comptime known.

Global read only memory is where literals get allocated, so when you have a pointer to a literal it must be const as it points to read only memory.

This does ignore the nuance of how the optimiser can change this

3 Likes

I actually thing your first post got me there. It gave me just enough to (I think) go look up all the pieces. Let me see if my understanding is correct:

  • A string literal like "asdf" is known at compile time and therefore constant.
  • It’s also an array of values, in this case a 4 item array of u8 values.
  • It’s also null-terminated and a pointer, so the type is *const [4:0]u8
  • Pointers to arrays can be coerced to slices
    • First convert to variable, non-sentinel array by using a pointer
    • Then convert to slice by using a reference

This seems to hold when printing the types:

const print = @import("std").debug.print;

pub fn main() void {
    const hello = "hello";
    var hello_non_sentinel: [5]u8 = hello.*;
    const hello_slice: []u8 = &hello_non_sentinel;
    print("{s}\n", .{@typeName(@TypeOf(hello))});
    print("{s}\n", .{@typeName(@TypeOf(hello_non_sentinel))});
    print("{s}\n", .{@typeName(@TypeOf(hello_slice))});
    print("\n", .{});

    var world_non_sentinel = "world".*;
    var words: [2][]u8 = .{ hello_slice, &world_non_sentinel };
    const words_slice: [][]u8 = &words;
    print("{s}\n", .{@typeName(@TypeOf(words_slice))});
}
// zig run scratch.zig
*const [5:0]u8
[5]u8
[]u8

[][]u8

You’re close enough for most practical purposes.

I would say it’s constant because it’s a literal, but comptime values are also always constant (at runtime) so it doesn’t matter much.

The array is null terminated, not the pointer.

You can do this in any order, infact you can do it in one step.


Something worth knowing, is that &.{a, b} where a and b are var/const is not a literal. If a and b are comptime known, then it will also be comptime known and be put into global read only memory.

If a or b is runtime known, then it will also be runtime known. In which case it does take up some stack memory, but it still can’t be mutable for a variety of reasons:

  • to keep it consistent with literals and comptime values
  • to simplify the language both its implementation and semantics
  • it at least keeps mutable memory explicit
  • it facilitates more optimisations

In the case where it is runtime known, it is called a temporary.
For the comptime case, it’s just a comptime value.

2 Likes