How to handle slice of structs when initialization a struct

This is the same problem I had with zon in Help for zon - #4 by mperillo, but with zig code.

Here is the code:

const std = @import("std");

const Activity = struct {
    workout: struct {
        name: []const u8,
        steps: []Step,
    },

    const Step = struct {
        name: []const u8,
    };
};

pub fn main() !void {
    const activity = Activity{
        .workout = .{
            .name = "aerobic_threshold",
            .steps = .{
                .{
                    .name = "warm-up",
                },
            },
        },
    };

    std.debug.print("{any}\n\n", .{activity});
}

The error message is:

activity-simplified.zig:18:23: error: type '[]activity-simplified.Activity.Step' does not support array initialization syntax
            .steps = .{
                     ~^
activity-simplified.zig:18:23: note: inferred array length is specified with an underscore: '[_]activity-simplified.Activity.Step'

If i change the declaration of the steps field from []Step to [1]Step, the program works correctly.

What is the correct syntax?

Thanks.

You need to take the address of the array you’re providing to turn it into a slice:

.steps = &.{
    .{
        .name = "warm-up",
    },
}

…similar to how imports are added to a module in build.zig:

const mod_host = b.addModule("host", .{
    .root_source_file = b.path("src/host/host.zig"),
    .target = target,
    .optimize = optimize,
    .imports = &.{
        .{ .name = "sokol", .module = dep_sokol.module("sokol") },
        .{ .name = "common", .module = mod_common },
        .{ .name = "shaders", .module = mod_shaders },
    },
});

Careful though if you return an Activity from a function, the .steps slice points to separate data on the stack and that will be gone upon function exit so you’ll get a dangling pointer (one important reason why I heavily prefer fixed-capacity nested arrays instead of slices in such situations - unfortunately Zig lacks some syntax sugar which would make working with nested arrays just as convenient as with slices).

PS:

If i change the declaration of the steps field from []Step to [1]Step

…you’re turning the slice (e.g pointer to an external array) into a nested array that lives inside the struct. Very important difference :slight_smile:

The slice syntax is a bit deceptive in that it looks and feels like an array, not like a pointer. But you need to pay the same attention to slices as with pointers, a slice is essentially a fat pointer with a (multi-item-)pointer and length inside.

2 Likes

The problem is that the code is still invalid:

activity-simplified.zig:18:14: error: expected type '[]activity-simplified.Activity.Step', found '*const [1]activity-simplified.Activity.Step'
            .steps = &.{
            ~^~~~~
activity-simplified.zig:18:14: note: cast discards const qualifier

Thanks.

Hmm true, you need to change the steps declaration to const:

steps: []const Step,

…that does indeed look a bit weird though… because it would mean that the steps slice content can never be mutable… but it’s the same thing the CreateModuleOptions struct does in the build.zig example I pasted:

The problem might be that an array literal is const and cannot be coerced to a var, which kinda makes sense in a way… a little detail which I haven’t noticed yet … but yet another advantage of nested fixed-capacity arrays :wink:

Thanks.

About constness, I tried to make the activity variable mutable, but I still got an error.

No that doesn’t help, since it’s the array literal on the right side of the steps assignment which is the problem. This basically forces everything else to be const.

You can definitely make variable array literals. I’m not sure where the syntax is getting caught up wrong, but i found that first declaring the array as a var, and then taking a reference to it works.

const std = @import("std");

const Activity = struct {
    workout: struct {
        name: []const u8,
        steps: []Step,
    },

    const Step = struct {
        name: []const u8,
    };
};

pub fn main() !void {
    var steps: [1]Activity.Step = [_]Activity.Step {
                    .{
                        .name = "warm-up",
                    }
                };
    const activity = Activity{
        .workout = .{
            .name = "aerobic_threshold",
            .steps = &steps,
        },
    };

    std.debug.print("{any}\n\n", .{activity});

}

As a related fun fact, this is the only way i know of to make comptime known variable string ‘literals’, as anything between quotes will be constant (and put in the read only section of data), you have to use array literal syntax to create a string that can be modified in place.

1 Like

Now thinking about it, I think the issue where you pull out the literal declaration but try to take the pointer at the same time makes sense.

    var steps: []Activity.Step = &[_]Activity.Step {
                    .{
                        .name = "warm-up",
                    }
                };

This doesn’t work because what is being declared as variable isn’t the underlying data, but the pointer. So you get a variable pointer to constant data. To get a pointer to varible data you must first declare the data to be var.

1 Like