Struct field not found using @field builtin function

Am I missing something here? The age field is found in one case but not in another. Is this an improper usage of @TypeOf ? I’ve poked at this for some time without gaining any understanding. Any help is appreciated.

const std = @import("std");
const Info = struct {
    name: []const u8,
    age: u8,
};
const InfoStorage = struct {
    const Self = @This();
    storage: [30]Info,
    pub fn readField(
        self: Self,
        index: usize,
        comptime field_name: []const u8,
    ) @TypeOf(@field(Info, field_name)) {
        const storage_ref = &self.storage[index];
        return @field(storage_ref, field_name);
    }
};

pub fn main() void {
    var info: InfoStorage = undefined;
    info.storage[0] = .{ .name = "Jane", .age = 42 };

    // The next line compiles and runs fine.
    std.debug.print("{d}\n", .{@field(&info.storage[0], "age")});

    // The next line fails to compile:
    // foo.zig:17:15: error: struct 'foo.Info' has no member named 'age'
    // ) @TypeOf(@field(Info, field_name)) {
    //           ^~~~~~~~~~~~~~~~~~~~~~~~
    // foo.zig:3:14: note: struct declared here
    // const Info = struct {
    //              ^~~~~~
    std.debug.print("{d}\n", .{info.readField(0, "age")});
}

The return type won’t work because you’re referencing a class-level member variable instead of an instance-level member variable.

So in the documentation, you’ll see these two examples:

const std = @import("std");

const Point = struct {
    x: u32,
    y: u32,

    pub var z: u32 = 1;
};

test "field access by string" {
    const expect = std.testing.expect;
    var p = Point{ .x = 0, .y = 0 };

    @field(p, "x") = 4;
    @field(p, "y") = @field(p, "x") + 1;

    try expect(@field(p, "x") == 4);
    try expect(@field(p, "y") == 5);
}

test "decl access by string" {
    const expect = std.testing.expect;

    try expect(@field(Point, "z") == 1);

    @field(Point, "z") = 2;
    try expect(@field(Point, "z") == 2);
}

Notice how in the first example, they are using an instance of Point called p. They then can get p’s member variables using the @field function.

In the second one, they are getting the field “z”. You’ll notice that the value z belongs to the class itself (kind of like a static variable in C++).

In your case, you are trying to access a field from a type, not from an instance of a type and that’s why it’s failing to compile.

Since we have a variable number of possible return types, I like to use helper functions to deduce these - to see examples of this, check out this thread: Implementing Generic Concepts on Function Declarations - #13 by AndrewCodeDev

Here’s an example that works using a helper function to deduce the type.

const std = @import("std");

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

pub fn InstanceFieldType(comptime T: type, comptime field_name: [] const u8) type {
    comptime var instance: T = undefined;
    return @TypeOf(@field(instance, field_name));
}

const InfoStorage = struct {
    const Self = @This();
    storage: [30]Info,
    pub fn readField(
        self: *Self,
        index: usize,
        comptime field_name: []const u8,
    ) InstanceFieldType(Info, field_name) {
        const storage_ref = &self.storage[index];
        return @field(storage_ref, field_name);
    }
};

pub fn main() void {
    var info: InfoStorage = undefined;
    info.storage[0] = .{ .name = "Jane", .age = 42 };
    std.debug.print("{d}\n", .{@field(&info.storage[0], "age")});
    std.debug.print("{d}\n", .{info.readField(0, "age")});
}

I’m also changing the self parameter type to be a pointer-to-self. It clarifies the intent that this is not supposed to be a copy.

– edited for for one more example –

You could also do this, but I don’t recommend it for the general case.

    pub fn readField(
        self: *Self,
        index: usize,
        comptime field_name: []const u8,
    ) @TypeOf(@field(self.storage[0], field_name)) { // adding &self.storage is optional here.
        const storage_ref = &self.storage[index];
        return @field(storage_ref, field_name);
    }

I don’t recommend it because accessing indices will at least require the array to have a size of 1. In your case, it’s kind of obvious it will work because you have 30 as the size. The reason I like the other method better is because it guarantees that there will be at least one instance available because we created it in the function itself.

1 Like

I’m going to add one more post here because I believe that the pointer cast is not doing what you’re intending and we can fix that too.

Consider this example:

const ref = &info.storage[0];

std.debug.print("{s}\n", .{ @typeName(@TypeOf(@field(ref, "age"))) });

You may expect that this should return a “const u8” - it does not. It only returns a u8. That’s because you have a “const-pointer-to-non-const” happening here. The pointer is const, the thing it’s pointing to is not. In other words, I can do the following:

ref.age = 5;

If you want a const reference to the thing you are returning, here’s what we can do:

const std = @import("std");

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

pub fn InstanceFieldType(comptime T: type, comptime field_name: [] const u8) type {
    const instance: T = undefined;
    return @TypeOf(&@field(instance, field_name));
}

const InfoStorage = struct {
    const Self = @This();
    storage: [30]Info,
    pub fn readField(
        self: *const Self,
        index: usize,
        comptime field_name: []const u8,
    ) InstanceFieldType(Info, field_name) {
        return &@field(self.storage[index], field_name);
    }
};

pub fn main() void {
    var info: InfoStorage = undefined;
    info.storage[0] = .{ .name = "Jane", .age = 42 };

    var x = info.readField(0, "age");

    std.debug.print("{s}\n", .{ @typeName(@TypeOf(x)) });

    std.debug.print("{d}\n", .{ x.* });
}

You’ll notice that it now prints:

*const u8
42

Otherwise, you are returning a copy because the @field function reads through the pointer to the member variable. I strongly recommend using the @typeName function to make sure you’re getting what you actually want.

In this case, we’re only returning a u8 or a slice - copies are fine here. If you are working with larger items like structs as members, that can be expensive.

In summary, if all you have is the type Info and the comptime string "age",

  • @TypeOf(@field(Info, "age")) returns the type of the declaration age (which doesn’t exist in your case), while
  • @TypeOf(@field(@as(Info, undefined), "age") returns the type of the field age.

@as(T, undefined) is a pretty useful one-size-fits-all solution for obtaining an instance of any type within a @TypeOf() expression. Expressions within @TypeOf() are guaranteed to have no runtime side effects so accessing members of the undefined instance works as long as you don’t evaluate their values (you will get a compile error if you do).

4 Likes

Thanks for your time constructing such considered answers. As life happens, it will take me a few days to study the answers and work on my mental model of Zig compilation. I’ll reply back when I have come to a better understanding. Thanks again.

1 Like

Of course - it’s interesting stuff. You can also combine what @castholm and I recommended together. Lots of good options here. One of them should suit your needs. :slight_smile:

Fields and Declarations and Types, oh my!

Having spent a little time absorbing all this, I thought I would reply back in my own “explain it to me like I’m a five year old” style in the hope I have understood things properly and that some other words might help someone else.

Zig structures have fields which serve as a template for how values and variables are lain out. But since Zig structures also serve the role of a container, they may have variable, constants and functions of their own. To know something about a field requires that a value or variable be the subject of the introspection. The structure type itself has no fields. None of this is conceptually unfamiliar to me, but clearly needed a kick-start of some ossified neurons to understand in the Zig context.

The suggestion of @castholm yields the answer in a concise use of three built-in functions. I works because:

  • undefined may be coerced to any type.
  • @as() yields a value of a given type even if the value is undefined.
  • @TypeOf() doesn’t care about the undefined nature of the value when it returns type information.

It’s a combination of reasoning I don’t think I would have arrived at on my own.

The suggetion of @AndrewCodeDev works off the same principle, namely to use @TypeOf() to retrieve a field type from a variable whose value is undefined. It is arguable a bit easier to understand for a less experienced Zig programmer.

Some time ago I was stumbling around in std.meta and I took another look there to see if there was any help. In std.meta there is another approach which uses a mapping onto the information returned by @typeInfo. The function FieldEnum() creates a type at comptime that is an enumeration of the fields of a type. The generated type has enumerators whose names are the same as the fields and whose enumerated values are the ordinal position of the field information in the type information. So, the return value from @intFromEnum() can be used directly as an index into the field information of the type. std.meta also provides the functions fieldInfo() and fieldType() to perform the computation on the type information (along with some error checking).

After some experiments, I decided to use the functions in std.meta because:

  • The field may be specified by an enumeration literal which is mnemonic to the manner a structure initializer is specified.
  • There is a well defined type to use as an argument.
  • It’s part of the standard library and has a special preference because of that.

The rework of the original post using the functions in std.meta is:

const std = @import("std");
const meta = std.meta;
const Info = struct {
    name: []const u8,
    age: u8,
};
const InfoStorage = struct {
    const Self = @This();
    storage: [30]Info,

    const Fields = meta.FieldEnum(Info);
    pub fn readField(
        self: Self,
        index: usize,
        comptime field: Fields,
    ) meta.FieldType(Info, field) {
        const storage_ref = &self.storage[index];
        return @field(storage_ref, meta.fieldInfo(Info, field).name);
    }
};

pub fn main() void {
    var info: InfoStorage = undefined;
    info.storage[0] = .{ .name = "Jane", .age = 42 };

    // The next line compiles and runs fine.
    std.debug.print("{d}\n", .{@field(&info.storage[0], "age")});

    // This time we use an enum literal as an encoding for
    // the field.
    std.debug.print("{d}\n", .{info.readField(0, .age)});
}

Sorry for the long post and thanks again for your help.

3 Likes

I’m wondering why a const pointer and not just self: Self? By doing the latter, I understand Zig will decide if using a pointer or not is the optimal course of action.

1 Like

You’re right. I suspect a copy-pasta error. The example was extracted from a larger interface. That coupled with a dose of fuzzy thinking. I’ll edit the post.

1 Like

I actually think this deserves it’s own topic at some point. Early on, I had some weird behavior because of the distinction here, but that was an old version of the compiler.

I remember in C++ when people would write their own destructors, it would disable other operations implicitly for the user. Unfortunately, that also meant people made copies all over the place unless they were familiar with how the compiler eliminates overload sets. Useful for people who knew what they were doing… weird for people who didn’t.

I think running some examples in different compiler settings (debug vs release) would make a really good addition for the site.

2 Likes

Good stuff! I’m going to mark @andy_mango’s final answer as the solution to this thread :slight_smile: