Zig generics: Why aren't generic struct subtypes treated as unique types?

I have here some sample code and I am curious why I can pass handles of type Resource2Handle to the printResource1 function.

Personally I would expect Resource1Handle and Resource2Handle types to be different types right? Or am I miss understanding zig generics?

const std = @import("std");

fn GenericTypeWithHandle(T: type) type {
    return struct {
        const Handle = struct {
            index: u32,
        };

        item: []const T,
    };
}

const Resource1 = struct {
    value: usize,
};

const Resource2 = struct {
    value: usize,
    value1: usize,
};

const GenericTypeWithHandleResource1 = GenericTypeWithHandle(Resource1);
const Resource1Handle = GenericTypeWithHandleResource1.Handle;

const GenericTypeWithHandleResource2 = GenericTypeWithHandle(Resource2);
const Resource2Handle = GenericTypeWithHandleResource2.Handle;

pub fn printResource1(resources: GenericTypeWithHandleResource1, handle: Resource1Handle) void {
    const value = resources.item[handle.index].value;
    std.debug.print("{d}", .{value});
}

pub fn printResource2(resources: GenericTypeWithHandleResource2, handle: Resource2Handle) void {
    const value = resources.item[handle.index].value;
    const value1 = resources.item[handle.index].value1;
    std.debug.print("{d} {d}", .{ value, value1 });
}

pub fn main() void {
    const resources1 = GenericTypeWithHandleResource1{ .item = &.{.{ .value = 1 }} };
    const resources2 = GenericTypeWithHandleResource2{ .item = &.{ .{ .value = 1, .value1 = 1 }, .{ .value = 2, .value1 = 2 }, .{ .value = 3, .value1 = 3 } } };

    const handle1 = Resource1Handle{ .index = 0 };
    const handle2 = Resource2Handle{ .index = 0 };

    printResource1(resources1, handle2);
    printResource2(resources2, handle1);
}

And if this is valid code, how can I achieve compile time error when passing handle2 to a function expecting handle1?

There’s no T parameter in your Handle, so they don’t depend on it. Do this and they’ll be different:

const Handle = struct {
    index: u32,

    comptime {
        _ = T;
    } 
};
3 Likes

Nice, feels a bit hacky but works :slight_smile: I had to adjust it a little bit to get it working though. I needed to discard it as follows:

comptime {
    const T1 = T;
    _ = T1;
}

Or I would get the error of: error: pointless discard of function parameter

This should work just as well:

const Handle = struct {
    index: u32,

    pub const ItemType = T;
};

This also lets you check the type passed to the handle with Handle.ItemType, which could come in handy. Arguably the least hacky way to do it.

3 Likes

Am I correct in assuming that adding the field has no runtime impact, since types are comptime values?

1 Like

That’s a declaration, not a field. So it adds ItemType to the Handle namespace, an instance handle can’t access it with field notation. That is, Handle.ItemType words, handle.ItemType wouldn’t. The latter only accesses fields, and declarations which are member functions: any function in the type’s namespace where the first argument type checks for an instance of that type, or a pointer to same.

This declaration will have no runtime existence, because it declares a comptime-only type, type in this case. Declarations of runtime types will exist in static memory (if const), but not as data attached to instances in any case. So no amount of declarations will increase the @sizeOf an instance of that container type. Only non-zero-size fields do that.

6 Likes