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