Array of arrays with default value, dont specify length

Hi,
Im having trouble with initializing an array of strings

pub const Foo = struct {
    bar: []const u8 = "this works ofc",
    foo: [][]const u8 = .{"this", "fails"},
};

I want a pointer anyway but

pub const Foo = struct {
    foo: *const [][]const u8 = &.{"this", "fails"},
};
const foo = [_][]const u8{"this", "fails"};

pub const Foo = struct {
    foo: *const [][]const u8 = &foo,
};

well im trying for foo to hold an slice, so lets pass an slice since it cant do the casting itself

const foo = [_][]const u8{"this", "fails"};

pub const Foo = struct {
    foo: *const [][]const u8 = foo[0..],
};

It wants me to specify foo: *const [2][] const u8, even for the pointer, even for the slice
(i specially dont understand this part, isnt []...supposed to be the syntax for a slice? why does it want me to specify the array length? then it wouldn be a slice??)

but later the contents of foo may change and may want to hold other number of items.
what am i missing? is it in my understanding of arrays and slices?

thanks in advanced

This is happens at comptime and everything is constant you need to do it like this:

const std = @import("std");

fn bar(s: []const []const u8) void {
    for (s) |val| {
        std.debug.print("{s} ", .{val});
    }
    std.debug.print("\n", .{});
}

pub fn main() !void {
    const foo: []const []const u8 = &.{ "this", "succeeds" };
    std.debug.print("{s} {s}\n", .{ foo[0], foo[1] });
    bar(foo);
    bar(&.{ "this", "also", "succeeds" });
}

Please note the second const.

In general the difference between a slice and an array is that the length of the array must be known at compile time. The length for a slice is generally not known at compile time. The zig compiler is generally quite strict. So when you try to declare a slice at compile time that has a known length it basically asks you if really want it to be a slice or an array.

Also note that in this case the slices point to constant memory, so they are basically an array.

2 Likes

i see, indeed adding that const works, but i dont think i understand why that is. seems like magic specially since i dont actually intend for it to be a const. But it works, and i can overwrite it like a var too. So whats the const doing?

You talk about arrays and slices, but it is not clear to me which of those you really want.
Additionally you demonstrate trying to declare constant nested slices but then you talk about wanting to modify them with var.

If you want to modify constant nested slices you essentially have to create a modified copy of the outer slice that is built from modified copies of the inner slices. So it is more like a functional-style-update of immutable values.

Do you want to use a multi-dimensional array or a nested slice?

You can look at this related topic that explains some of the options and how they work:

Please clarify your goal/intention, what are you trying to create?


The type of this is a mutable slice of constant string-slices, the default value the part behind = needs to be something that can be evaluated at comptime, because it is the default value that is used for every instance of Foo.

You can get it to work with this (somewhat cursed) code and I don’t really think you should use it or even want this:

var data = [_][]const u8{ "this", "fails" };
const defaults: [][]const u8 = &data;

pub const Foo = struct {
    // NOTE this example demonstrates why using [][]const u8 is undesireable
    // NOTE all the default constructed Foo-instances point to the same container-level-variable
    bar: []const u8 = "this works ofc",
    foo: [][]const u8 = defaults,

    pub fn format(
        self: @This(),
        writer: *std.Io.Writer,
    ) std.Io.Writer.Error!void {
        try writer.writeAll("Foo\n");
        try writer.print("  bar: {s}\n", .{self.bar});
        try writer.writeAll("  foo:\n");
        for (self.foo) |s| {
            try writer.print("    {s}\n", .{s});
        }
    }
};

pub fn main() !void {
    const f1: Foo = .{};
    std.debug.print("f1: {f}\n", .{f1});
    data[1] = "changes everywhere";
    const f2: Foo = .{};
    std.debug.print("f1: {f}\n", .{f1});
    std.debug.print("f2: {f}\n", .{f2});
}

const std = @import("std");

I think the more appropriate thing to do would be to use []const []const u8 and then you can do things like this:

pub const Foo = struct {
    bar: []const u8 = "this works ofc",
    foo: []const []const u8 = &.{ "this", "fails" },

    pub fn format(
        self: @This(),
        writer: *std.Io.Writer,
    ) std.Io.Writer.Error!void {
        try writer.writeAll("Foo\n");
        try writer.print("  bar: {s}\n", .{self.bar});
        try writer.writeAll("  foo:\n");
        for (self.foo) |s| {
            try writer.print("    {s}\n", .{s});
        }
    }
};

pub fn main(init: std.process.Init) !void {
    const f1: Foo = .{};
    std.debug.print("f1: {f}\n", .{f1});

    var f2: Foo = f1;

    const gpa = init.gpa;
    const parts = try gpa.alloc([]const u8, 3);
    defer gpa.free(parts);
    @memcpy(parts, &[_][]const u8{ f1.foo[0], "works", "now" });
    f2.foo = parts;

    std.debug.print("f1: {f}\n", .{f1});
    std.debug.print("f2: {f}\n", .{f2});
}

const std = @import("std");

The thing with slices is to understand and think about their memory layout, they are a pointer plus a length so essentially like a struct { ptr: [*]T, len: usize }, this needs memory which makes nested slices a lot more complicated, than multi-dimensional arrays (because for those all the lengths can be computed statically).

2 Likes

A slice is a pointer and a length. Arrays do not automatically convert to slices, but pointers to arrays do. So to create a slice from an array, you have to use &array.

String literals are pointers to an array, for example "this" is *const [4]u8, this will automatically convert to a slice: []const u8.
.{"this", "fails"} is an array of 2 string, &.{"this", "fails"} is a pointer to an array of two strings, which can be converted to a slice of strings.

All those pointers and slices have to be const, because they point to static memory, and you can not modify static memory. That makes sense, because you obviously can’t do

const pointer_to_int: *i32 = &4;
pointer_to_int.* = 5;

To be able to modify it, you need to put it in a var variable, or allocate it. If you want to work with a mutable array of strings, you may want to use std.ArrayList.


An illustration may make things clearer. If you would use this:

pub const Foo = struct {
	bar: []const u8 = "this works ofc",
	foo: []const []const u8 = &.{"this", "too"},
};

It looks like this in memory:

In []const []const u8, the first const is protecting you from modifying the blue part in the image and the second const is protecting modification of the strings ‘this’ and ‘foo’.
You can reassign Foo.foo (if you store Foo in a var), because you are then modifying the red part by replacing the pointer/slice, you are not modifying the content of the array.

4 Likes

im making a struct that needs a list of arbritary number of strings. needs to have a default value at initialization, so that by necessity it seems to be const, but the contents may change later on. Actual usage, loading a config file, so it has a default, if the config file has a value for that item, overwrite it, and later also maybe overwrite from cli input.

I talk about slices coz from my current understanding zig wants to you to use slices as much as possible when doing arrays.

So it would be a bad idea to do it like that? I imagine if changing more than once, having to allocate for them, we may easily end with dangling pointers. But whats the alternative? ArrayList feels too cumbersome for this, is it the simplest way? (also would have to look into it again, but i dont remember a single line way to initialize an ArrayList directly with some contents in it (for declaring in a struct). maybe with fromOwnedSlice).
Really nice diagram btw :slight_smile:

Thanks for the explanations to both :smiley:

you may not need it to be mutable at all if you are able to create the struct after you get that data.

Even if that is not possible, you only need the struct to be mutable, not the strings themselves.

Just free them before you update them… make this a function so you dont have to remember. This requires you either have a way to distinguish between the non allocated default case, or to always allocate the default case.

good code does not mean less code, you will make it harder for yourself if you try to optimise for fewer lines of code.

Pedantic: zig wants you to do whatever is best for your use case, often this means (for parameters and fields) to use slices instead of arrays as they are just more versatile. But there are plenty of cases where arrays are the best; and when you have direct access to an array instead of a slice there is no reason not to use it.

Less code is not the metric but readability is extremely important. And blocks have less of it.
Even more so if we are talking about a block of code for the default value of a field inside a struct.

And sure, in this case its going to be, what? 2 lines? create the arraylist and populate it? (and im not even sure this can be done, im imagining it would (having a struct field get its default value from a block i mean))

pub const Foo = struct {
    foo: ArrayList([]const u8) = blk: {
        var tmp = ArrayList([]const u8).empty;
        break :blk tmp.fromOwnedSlice(gpa, .{"this", "works"});
    },
};

but its going to be less readable than a simple foo: ArrayList([]cons u8) = .initWithValues(gpa, .{"this", "works"}); if such a method existed.
Which, as an aside, now that im writing it, im pretty sure that Neither of those can be done, because i dont think you can have the gpa inside the struct declaration; you would need to create an init method for the struct to fill that field at creation. Yeeesh, so messy.

Programming languages often are and zig certainly is. lol
And you cant blame someone for trying to learn the idiomatic patterns.

1 Like

I experimented a bit and found something that works:

var foo = [_][]const u8{"this", "works"};

pub const Foo = struct {
    foo: *const [][]const u8 = &&foo,
};

Explanation:

  • A pointer to foo can be automatically coerced to [][]const u8 as long as foo is mutable (var)
  • A pointer to a pointer to foo can therefore be coerced to *const [][]const u8 by the same rules

Yes, but more importantly, field defaults must be comptime known, and heap allocation doesnt work at comptime (and currently, neither to non heap allocators). So in that sense, you dont have to worry about it.

Also fromOwnedSlice takes a mutable allocated slice; comptime data (given to runtime) is always immutable, and a non allocated slice will cause illegal behaviour when the list tries to grow, or deinit.

No blame given, I was just clarifying.

If you think in high level terms, sure. But zig is not that kind of language, it makes many important details visible.

Take rust as an example, it can do the one-liner you want
vec!["this", "works"], it creates a vector (ArrayList), containing the given values; But there are glaring questions any systems programmer would ask:

Is it pointing to constant data? that would mean it has to be copy-on-write (cow hehe) - no, it copies to heap

Does it allocate upfront? since that is an easy and obvious performance and efficiency gain. - yes, if you know a little about rust macros you might be confused how thats possible XD

What allocation strategy does it use? - global allocator (rusts allocator api is just worse)

What happens if allocation fails? - unrecoverable panic

All of those require existing knowledge, or digging to find it in language/stdlib docs.

compared to zig:

var list: ArrayList([]const u8) = .empty;
try list.appendSlice(gpa, &.{"this", "works"});

Is it pointing to constant data? that would mean it has to be copy-on-write (cow hehe) - you give it an allocator, so it likely does

Does it allocate upfront? since that is an easy and obvious performance and efficiency gain. - yes, tbf you’d need to read docs/src to know that

What allocation strategy does it use? - Chosen by the allocator you give it

What happens if allocation fails? - it clearly errors

3/4 being plainly visible in source without digging seems like measurably more readability to me…
Ofc that is amount of information, not ease of reading it, that is subjective, IMO it is quite easy to read.

4 Likes

hahahaha thanks, slightly cursed