How to get a struct pointer from a field pointer and keep them have the same const-ness?

This code works:

const std = @import("std");

const S = struct {
    t: T,
    
    fn getT(self: anytype) @TypeOf(&self.t) {
        return &self.t;
    }
};

const T = struct {
    x: bool,
};

pub fn main() !void {
    var s = S{ .t = .{.x = true} };
    const cs = s;
    
    std.debug.print("{}\n", .{@TypeOf(s.getT())}); // *main.T
    std.debug.print("{}\n", .{@TypeOf(cs.getT())}); // *const main.T
}

But this doesn’t, since now both @alignCast and @fieldParentPtr do result deduction, we can’t pass S in.

const std = @import("std");

const S = struct {
    t: T,
};

const T = struct {
    x: bool,
    
    fn ownerS(self: anytype) @TypeOf(@alignCast(@fieldParentPtr("t", self))) {
        return @alignCast(@fieldParentPtr("t", self));
    }
};

pub fn main() !void {
    var t = T{.x = true};
    const ct = t;
    
    std.debug.print("{}\n", .{@TypeOf(t.ownerS())});
    std.debug.print("{}\n", .{@TypeOf(ct.ownerS())});
}

I indeed find a way, but many ownerType alike helper functions might be needed for a project.

const std = @import("std");

const S = struct {
    t: T,
};

const T = struct {
    x: bool,
    
    fn ownerType(TT: type) type {
        return switch (TT) {
            *T => *S,
            *const T => *const S,
            else => unreachable,
        };
    }
    
    fn ownerS(self: anytype) ownerType(@TypeOf(self)) {
        return @alignCast(@fieldParentPtr("t", self));
    }
};

The distinction between *const T and * T is meaningless, in fact @fieldParentPtr does not accept *const T

const std = @import("std");

const S = struct {
    t: T,
};

const T = struct {
    x: bool,

    fn ownerS(self: *T) *S {
        return @alignCast(@fieldParentPtr("t", self));
    }
};

This is the only sensible way to write it. You shouldn’t expect ownerS to take a *const T parameter.

Supplement: You should not expect @fieldParentPtr to have the ability to automatically deduce the parent structure type. It is impossible to do this. In fact, this is a built-in function based on result type deduction. It can only execute normally if it knows the type of its result position, and the result position type needs to be specified by you.

I think a function like AutoPtr below can be used instead, as a compromise between versatility and being concise. (But it may make sense to lock-down/harden the function more to only accept pointers that can be method receivers)

How is the distinction meaningless?
When constness has real consequences on what methods you can call on the result to ownerS?

And why can I write this then?
zig 0.15.1:

const std = @import("std");

const S = struct {
    t: T,

    pub fn format(self: *const S, writer: anytype) error{WriteFailed}!void {
        try writer.print("t: {}\n", .{self.t});
    }

    pub fn toggle(self: *S) void {
        self.t.x = !self.t.x;
    }
};

const T = struct {
    x: bool,

    fn ownerS(self: anytype) AutoPtr(@TypeOf(self), S) {
        return @alignCast(@fieldParentPtr("t", self));
    }
};

pub fn AutoPtr(input: type, Output: type) type {
    return switch (@typeInfo(input)) {
        .pointer => |p| if (p.is_const) *const Output else *Output,
        else => @compileError("not supported"),
    };
}

pub fn main() !void {
    var s = S{ .t = .{ .x = true } };
    const cs = s;

    const t: *T = &s.t;
    const ct: *const T = &cs.t;

    std.debug.print("{}\n", .{@TypeOf(t.ownerS())});
    std.debug.print("{}\n", .{@TypeOf(ct.ownerS())});
    std.debug.print("{f}\n", .{t.ownerS()});
    std.debug.print("{f}\n", .{ct.ownerS()});

    t.ownerS().toggle();
    // ct.ownerS().toggle(); // not allowed on const pointer

    std.debug.print("{f}\n", .{t.ownerS()});
    std.debug.print("{f}\n", .{ct.ownerS()});
}

It seems to me that the language reference is wrong describing the signature with:

@fieldParentPtr(comptime field_name: []const u8, field_ptr: *T) anytype

When the function empirically accepts either *T or *const T (or any kind of pointer?).
So is the documentation wrong or the language implementation?
Considering there is no existing notation for an arbitrary pointer (or field pointer), I think using *anytype (which does not exist) to describe it, would possibly confuse readers into thinking that it is a valid variation of anytype. (And just using anytype wouldn’t communicate that it needs to be a pointer)

I think as long as the pointer is actually a pointer to a field of the corresponding @fieldParentPtr result type, it should work; and the properties of the field_ptr should not matter if it is a valid field pointer.

There is actually another error about the documentation:

Given a pointer to a struct field, returns a pointer to the struct containing that field.

You also can use @fieldParentPtr to convert a pointer to a union-field-value, to a pointer to the union value.

The documentation only mentions structs and implies it only works with structs.

2 Likes

Thanks for pointing out the actual behavior. Indeed, @fieldParentPtr actually allows you to receive a const pointer and return a const result pointer.

However, in practice, I tend not to do this, even if you really want to use it “read-only.” This is because substructures involving @fieldParentPtr should not be copied, and a const substructure can give the misconception that it can be safely copied.

Personally, I won’t use @fieldParentPtr to convert a const pointer to a const parent structure pointer to construct an API. This makes it impossible for me to copy a substructure as const whose API uses @fieldParentPtr and still use it normally, so I can be wary of this copying behavior.

My understanding of the anytype return value in built-in functions is that the function’s return type is inferred backwards based on the result location.

This is a privilege limited to built-in functions. Other than built-in functions, all user-constructed functions cannot implement return type inference based on result location. I have some expectations for this functionality being implemented in non-built-in functions, perhaps with a @RetrunLocation() method similar to @This() to express this type.

I think this currently happens to work, but isn’t officially supported by the language and is technically illegal behavior, because @fieldParentPtr states:

If field_ptr does not point to the field_name field of an instance of the result type, and the result type has ill-defined layout, invokes unchecked Illegal Behavior.

For it to be technically correct you would need to have:

var s = S{ .t = .{.x = true} };
const cs = s;

const t:*T = &s.t;
const ct:*const T = &cs.t;

The “simpler” version only works because struct currently doesn’t use all the freedom it has to create arbitrary struct layouts and thus S and T happen to have compatible layouts by coincidence (and because S only has a single field) and because the illegal behavior of @fieldParentPtr is unchecked, you don’t get any error.

If Zig for example decides in the future to add some kind of debug meta data to normal structs (similar to the tag in tagged unions), or randomize struct layouts to keep people from using non-guaranteed semantics or optimizes struct-layouts differently based on how they are used, then I think the “simple” version would fail.

1 Like

True. Sorry, I was hurry up to make that demo example.

Thanks for the AutoPtr solution suggestion.

1 Like