Struct Field Documentation (0.11.0)

Hey y’all,

I’ve been looking through the documentation on structs, but I am struggling to understand fully the behavior of fields and specifically how they relate to instances versus the struct type. ZLS has guided me on a few restraints regarding field declaration (see below), but I am still a bit lost. Could someone point me towards some documentation I might’ve missed, or explain to me some of the behavior when it comes to fields? (Zig==0.11.0)

Declaration testing:

arr: [1]u8,  // OK
arr: [1]u8 = undefined,  // OK
comptime arr: [1]u8,  // "error: comptime field without default..."
comptime arr: [1]u8 = undefined,  // OK
arr: [1]u8;  // "error: expected ',' after field..."
var arr: [1]u8 = undefined;  // OK, but not accessible via the instance.
var arr: *[1]u8 = undefined,  // Compiles, but causes segfault on assignment (e.g. arr.*[index] = value)

This question originates from a problem. In this scenario, I would ideally like a variable struct field;

fn Generic(comptime T, comptime a: i64) type {
    return struct {
        const Self = @This();
        arr: [a]T = undefined;

        fn new() Self { return .{}; }

        fn set(s: Self, val: T, idx: usize) {
            s.arr[idx] = val;
        }
    };
}

test {
    const G1 = Generic(f32, 1);
    const g1_inst = G1.new();
    g1_inst.set(1.0, 0);
}

Yielding this error: error: cannot assign to a constant.

Thanks!

fn Generic(comptime T: type, comptime a: i64) type {
    return struct {
        const Self = @This();
        arr: [a]T = undefined,

        fn new() Self { return .{}; }

        fn set(s: *Self, val: T, idx: usize) void {
            s.arr[idx] = val;
        }
    };
}
const G1 = Generic(f32, 1);
var g1_inst = G1.new();
g1_inst.set(1.0, 0);
return g1_inst.arr[0];

I fixed a couple typo’s for ya there, but the main thing is that your instance needs to be var and not const, and your set function needs to take in a *Self, not a direct Self parameter (which causes it the parameter to become constant for various reasons).

1 Like

I think both lines are kind of questionable / not really useful in practice, I think a comptime field is basically a field that has a fixed value but can be accessed as if it was a field of an instance, but in reality it is more like a constant that the compiler may optimize out where possible. At least that is how it seemed to me from what I have learned so far.

So considering that you can’t really change the value of the second line and the value is undefined, the cases where this is useful are quite rare. (and probably involve quite a bit of meta programming)

fields end with , and statements with ;

It is likely you are defining a Container Level Variable with that line.

Here you are de-referencing a pointer that points to random garbage, because the pointer was undefined. Getting a segfault when de-referencing a garbage pointer is the best outcome.

What you actually want is to use a self parameter that is a pointer so that your method can mutate the instances field:

fn set(s: *Self, val: T, idx: usize) void {
    s.arr[idx] = val;
}

But personally I would tend to either remove the default value from the arr field, requiring to set the field in the new function during creation of the instance:

        arr: [a]T;

        fn new() Self { return .{.arr = [1]T{0} ** a}; }

Or I would tend to give it a zero default value instead of undefined:

        arr: [a]T = [1]T{0} ** a,

There are cases where you really want to use undefined but I think they are a bit less common, it is often good to use some default zero initialized values.

In this case that would make the set call unnecessary if you default initialize to zero:

    const G1 = Generic(f32, 1);
    const g1_inst = G1.new();

If you don’t zero initialize you now potentially have a problem if you try to read or use one of the values, before you have set them, so that is why I would avoid that unless I have actually measured that this is beneficial for performance.

2 Likes

Oh I was wrong the parameter order of your set method is unexpected, I am so used to key value or index value. Tricking me with unusual parameter order… :laughing:

1 Like

Thanks for the quick replies y’all! So, if I’m understanding correctly;

  • Fields “inherit” their const/var-ness from the struct instances’ declarations. If that’s true, then does this mean that in memory, the layout of a const struct is immutable?
  • In this case, in order to update a field, I would need the method to reference a pointer to the struct (which makes sense cause all parameters are constant).
  • Comptime is generally redundant for fields, but might provide some optimizations, but what about the case when the struct instance is a variable?

I’m a trickster, gotta keep y’all on your toes >:)

(seriously though thanks for all the help)

2 Likes

Comptime fields are kind of fake fields.
Normal fields just have a type and that type can be a u32, but you can’t define a const u32 field, because the const-ness is either via constants/variables or you can have a pointer to a constant, in that case it is a property of the pointer.

But you can sort of think of a field as constant if you have defined the instance as constant. For more info: Mutable and Constant Pointer Semantics

Can you rephrase what you mean with “what about the case when the struct instance is a variable?”


Just as a side-note:

Layout in Zig is often associated with field layout order and memory layout, so basically paddings between fields and so on, but that is relatively orthogonal to the whole concept of things being const.
Const is just a property on certain language constructs that is used to track whether something can/should be mutable, it doesn’t directly tell you whether something actually could be changed, it only helps you in avoiding mutating things that shouldn’t be mutated.

For example you can have a mutable memory region and within that a struct instance and you can have a mutable or a const pointer to that, with the former you can read and write to the memory, but with the latter you only can read the memory.

Now if you somehow know that the underlying memory is mutable, you can constCast the latter pointer to a mutable pointer and write to it anyway.

But now someone decided that you shouldn’t do that and used page mapping functions to set the memory to read only, now you can still const cast to get the mutable pointer, but the moment you write to it, you will get a segfault.

But now the former can’t write to it neither, but there is a whole wondrous rabbit hole, of interesting page mapping techniques, that I will have to explore with my own experiments in more detail some day. In this case page mapping could be used to map the same physical memory page at two different logical page addresses, where you set one to read only and the other to read/write.

Would be cool if that was some day optionally used in Zig to ensure that certain things aren’t modified, through for example pointers that shouldn’t be used to modify things.

But I don’t know enough about how safety checks are implemented, whether that would be one of the techniques there.

I’d like to add one more piece here because comptime fields are actually quite powerful.

You can scan over them using @typeInfo in a loop and do some powerful reflection techniques with them. Here’s an example where I’m using it to run over auto-generated types to pickup if they have specific functions (those functions are comptime fields):

    const decls = CallbackType{};

    inline for (@typeInfo(CallbackType).Struct.fields) |field| {
        if (comptime reversePrefixed(field.name)) {
            const edge_index = @field(decls, field.name).edge_index;

            if (last_edge <= edge_index) {
                const next_node = reverseEdge(
                    @field(decls, field.name).callback,
                    edge_index,
                    edge_tuple,
                );
                // more stuff...

The thing is, if you’re using comptime fields, you probably have a specific use-case in mind and that struct is probably acting in a specialized way… here’s an example from another forum post we had on the same thing: How to iterate over struct declarations? - #3 by AndrewCodeDev

Just some food for thought. I wouldn’t rule them out, but they’re not as common, for sure.

I think what @Sze means to say is that they don’t have state like a normal field does. A comptime int isn’t stored in the same way that a runtime int is. If you look at their size, for instance with @sizeOf, you’ll see that it’s zero.

pub fn main() !void {
    const x: comptime_int = 1;
    std.debug.print("{}\n", .{ @sizeOf(@TypeOf(x)) }); // prints zero
}
1 Like

They are useful beasts for certain cases, but they have special behavior, they also seem a bit under documented. I played with them a bit, but I wouldn’t be surprised if there still was some detail about them I don’t know.

I was also meaning to create a brainstorming topic about a fun trick with them (that doesn’t seem super useful), so maybe I should do that.


Edit: Ahh I found the issue, it wasn’t about comptime fields, but about anonymous struct types: Remove anonymous struct types from the language · Issue #16865 · ziglang/zig · GitHub

Yeah, I’d definitely like to see what you’ve got. I’m interested in this update you’re talking about so hopefully if we open up a topic we can get someone to comment on that.

I did a bit of testing locally on this. Basically, would a comptime (or normal) field be affected when the struct’s instance is a var vs a const.

I checked out the sizeOf and the typeInfo and, to me, there was no discernible difference in the field behavior.

Edit: Except in the case of integers like @AndrewCodeDev describes above.

Comptime fields have size zero, normal fields have their normal size.

var vs const on the instance influences whether you can assign the field via instance.field = value the special thing here with comptime fields is that you only can assign the value they already have, which results in no change.

var vs const on the instance doesn’t change the size of the instance type.
However if a field is comptime it will not add to the instance types size and you won’t be able to give it a different value at run time. (Because comptime fields essentially only exist syntactically but not as a actual runtime field, that can be written to)

2 Likes

I think @Sze is pointing you in the right direction. They really aren’t the same thing even though syntactically speaking, they look similar. @peanut, if I were you, I’d make a separate study of them and create some examples to build your familiarity.

1 Like

Okay here are some examples on why this tripped me up. @Sze’s explanation makes sense to me, however in my testing I found that when setting comptime array fields, the behavior differed. I guess because the length of an array must be known at compile time.

test "comptime_var_struct" {
    // Comptime integer
    const S1 = struct {
        comptime f: comptime_int = 0,
    };
    std.debug.print("\nsizeOf S1 with comptime_int: {}\n", .{@sizeOf(@TypeOf(S1))});
    comptime var s1v = S1{};
    std.debug.print("sizeOf S1 instance with comptime_int: {}\n", .{@sizeOf(@TypeOf(s1v))});
    std.debug.print("sizeOf comptime_int var instance: {}\n", .{@sizeOf(@TypeOf(s1v.f))});
    const s1c = S1{};
    std.debug.print("sizeOf comptime_int const instance: {}\n", .{@sizeOf(@TypeOf(s1c.f))});

    // Regular array field
    const S2 = struct {
        f: [1]u8,
    };
    std.debug.print("\nsizeOf S2 with array field: {}\n", .{@sizeOf(@TypeOf(S2))});
    var s2v = S2{ .f = [1]u8{1} };
    std.debug.print("sizeOf S2 instance with array field: {}\n", .{@sizeOf(@TypeOf(s2v))});
    std.debug.print("sizeOf array field var instance: {}\n", .{@sizeOf(@TypeOf(s2v.f))});
    const s2c = S2{ .f = [1]u8{1} };
    std.debug.print("sizeOf array field const instance: {}\n", .{@sizeOf(@TypeOf(s2c.f))});

    // Comptime array field
    const S3 = struct {
        // Must be initialized since it's comptime
        comptime f: [1]u8 = undefined,
    };
    std.debug.print("\nsizeOf S3 with comptime array: {}\n", .{@sizeOf(@TypeOf(S3))});
    var s3v = S3{};
    std.debug.print("sizeOf S3 instance with comptime array: {}\n", .{@sizeOf(@TypeOf(s3v))});
    std.debug.print("sizeOf comptime array var instance: {}\n", .{@sizeOf(@TypeOf(s3v.f))});
    const s3c = S3{};
    std.debug.print("sizeOf comptime array const instance: {}\n", .{@sizeOf(@TypeOf(s3c.f))});

    // Just understanding arrays
    comptime var arr1 = [1]u8{1};
    comptime var arr2: [1]u8 = undefined;
    const arr3: [1]u8 = undefined;
    std.debug.print("arr1: len({}) ptr({*}) sizeOf({})\n", .{ arr1.len, &arr1, @sizeOf(@TypeOf(arr1)) });

    std.debug.print("arr2: len({}) ptr({*}) sizeOf({})\n", .{ arr2.len, &arr2, @sizeOf(@TypeOf(arr2)) });

    std.debug.print("arr3: len({}) ptr({*}) sizeOf({})\n", .{ arr3.len, &arr3, @sizeOf(@TypeOf(arr3)) });
}

Results:

Test [1/1] test.comptime_var_struct...
sizeOf S1 with comptime_int: 0
sizeOf S1 instance with comptime_int: 0
sizeOf comptime_int var instance: 0
sizeOf comptime_int const instance: 0

sizeOf S2 with array field: 0
sizeOf S2 instance with array field: 1
sizeOf array field var instance: 1
sizeOf array field const instance: 1

sizeOf S3 with comptime array: 0
sizeOf S3 instance with comptime array: 0
sizeOf comptime array var instance: 1
sizeOf comptime array const instance: 1
arr1: len(1) ptr([1]u8@203a1a) sizeOf(1)
arr2: len(1) ptr([1]u8@203a1b) sizeOf(1)
arr3: len(1) ptr([1]u8@204668) sizeOf(1)
All 1 tests passed.