Type T as an array index or hashmap key

While perusing cpp code I’ve noticed two patterns I’m interested in,
first the less interesting:

Q1: Type T as a hashmap key

In cpp they use std::type_index(typeid(T)), closest thing in zig I
know of is @typeName(T), which can be used as a key of
std.StringHashMap. But as someone who knows nothing about how
hashmaps works, I am convinced an usize would be much more efficient
than a []const u8. I actually got something to work, only anonymous
structs survive optimization, but it is quite goofy and might stop
working at any time I guess.

Question: Is there any better way and does avoiding
std.StringHashMap make sense ? These systems get fetched every frame
of a game, I’d imagine it makes a difference, and that’s also why I
would avoid hashmaps completly in favour of Q2.

//! zig version: 0.16.0

const std = @import("std");

const AudioSystem = struct {
    // pub const Index: usize = 0;
    // optimization kills the option above, only anonymous structs work
    pub const Index = struct { x: u8 = 0 }{};
};

const RenderSystem = struct {
    pub const Index = struct { x: u8 = 0 }{};
};

const SystemManager = struct {
    map: std.AutoHashMap(usize, usize),

    fn put(self: *@This(), T: anytype, val: usize) !void {
        try self.map.put(@intFromPtr(&T.Index), val);
    }

    fn get(self: @This(), T: anytype) ?usize {
        return self.map.get(@intFromPtr(&T.Index));
    }
};

pub fn main(init: std.process.Init) !void {
    var manager: SystemManager = .{ .map = .init(init.arena.allocator()) };
    try manager.put(AudioSystem, 67);
    try manager.put(RenderSystem, 420);

    std.debug.print(
        \\audio  {?d} {*}
        \\render {?d} {*}
        \\
    , .{
        manager.get(AudioSystem),  &AudioSystem.Index,
        manager.get(RenderSystem), &RenderSystem.Index,
    });
}

Output:

$ zig build-exe -O ReleaseFast test.zig ; ./test
audio  67 test.AudioSystem.Index__struct_2267@100ec68
render 420 test.RenderSystem.Index__struct_2281@100ec69

Q2: Type T as an index into an array

Blah blah blah something called static methods apparently, hard to
understand cppnese. Every Component T should know its index into an
array, where a pool of said components is stored.

The only solution I could think of:

const CompId = enum(usize) {
    hp,
    mana,
};

const ManaComp = struct {
    pub const Id: usize = @intFromEnum(CompId.mana);
};

const HpComp = struct {
    pub const Id: usize = @intFromEnum(CompId.hp);
};

fn getId(T: anytype) usize {
    return T.Id;
}

Question: This works, but is there some magic where a function at
comptime could return a global incrementing counter to populate these?

There is a bit of a hybrid approach where you use std.StaticStringMap built during comptime to produce and get indices.
You’ll have to manually add all the types you’re interested in, but all get operations are pre-computed:

const type_index_map = blk: {
    const Tuple = struct { []const u8, usize };
    var kvs: []const Tuple = &.{};
    for (.{
        @typeName(ManaComp),
        @typeName(HpComp),
    }, 0..) |name, i| {
        const v: Tuple = .{ name, i };
        kvs = kvs ++ .{v};
    }

    break :blk std.StaticStringMap(u32).initComptime(kvs);
};

pub fn type_index(comptime T: type) u32 {
    return comptime type_index_map.get(@typeName(T)).?;
}

A bit of a shame, but actually it can’t do it.

related

Try AutoHashMap if you don’t want to write the hash function yourself.

This reminds me a bit of this topic:

related topics:


I think my preferred solution is to have something like this (if all your components are known at comptime):

const ManaComp = struct {};

const HpComp = struct {};

const NewComp = struct {}; // not registered yet

const ComponentId = enum(u32) { _ };
pub fn componentId(comptime ComponentType: type) ComponentId {
    // NOTE linear search here should be un-problematic unless you have a huge number of components
    // because the comptime result of this function is memoized anyway
    return @enumFromInt(comptime std.mem.findScalar(type, &components, ComponentType) orelse @compileError("please add missing Component: " ++ @typeName(ComponentType) ++ " to the components array"));
}
const components = [_]type{
    HpComp,
    ManaComp,
    // NewComp,
};

const std = @import("std");

pub fn main() !void {
    std.debug.print("HpComp id: {}\n", .{componentId(HpComp)});
    std.debug.print("ManaComp id: {}\n", .{componentId(ManaComp)});
    std.debug.print("NewComp id: {}\n", .{componentId(NewComp)});
}

A good question to ask yourself is whether you need components which are registered at runtime, if you don’t need them you can take advantage of simple comptime code like this, where you just add all components to this components array (getting an error if you forget) and you get a stable and reliable id for each component in return.

So basically this gives you a reliable index for your type, simply by enforcing that your type is part of this specific array that is used to determine / declare its index.

2 Likes

@Sze
Thank you this is exactly what I wanted, wasn’t aware you can keep types in an comptime array.

Ended up with the code below to mimic static methods, I like it, the only issue I’ve had with the enum approach were the inevitable copy-paste bugs, impossible with this one.

const ManaComp = struct {
    pub const Id: usize = componentId(@This());
};

@npc1054657282
Very good to know, thank you, of course it would truncate the name at some point haven’t thought of that.

1 Like