I want to have a “system” that can contain a closed set of types of objects, my idea here is, that all the types of objects this “system” can hold must meet some requirements for them to be useful-for instance a “GameUpdate” system that can hold only objects which have an “update” function definition, so i came up with this:

const Dog = struct{
    kind: DogKind,

    pub fn speak(self: Dog) void{
        std.log.info("woof woof i am {s}", @tagName(self.kind));
    }
};

const DogKind = enum{
    Dachshund,
    Corgi,
    Alaskan,
    Bulldog,
};

const Cat = struct{
    is_garfield: bool = false,

    pub fn speak(self: Cat) void{
        if (self.is_garfield){
            std.log.info("give me lasagna");
            return;
        }
        std.log.info("meow i am cat");
    }

const AnimalUpdate = struct{
    //the closed set of types i was referring to
    const available_types = [_]type{Dog, Cat};
    
    //here the pointers to ArrayLists of each of the above types are stored
    storages: [AnimalUpdate.available_types.len]*anyopaque,

    pub fn init(allocator: std.mem.Allocator) AnimalUpdate{
        var animal_update: AnimalUpdate = undefined;

        for (0..available_types.len) |type_index|{
            const animals = try allocator.create(std.ArrayList(available_types[type_index]));
            animals.* = .empty;

            animal_update[type_index] = animals;
        }        
    }

    pub fn get(self: AnimalUpdate, comptime T: type) *std.ArrayList(T){
        switch(T){
            Dog => return @alignCast(@ptrCast(self.storages[0])),
            Cat => return @alignCast(@ptrCast(self.storage[1])),
            else => @compileError("unsupported type"),
        }
    }

    pub fn update(self: AnimalUpdate) void{
        for (0..available_types.len) |type_index|{
            const animals = self.get(available_types[type_index]);

            for (animals.items) |animal|{
                animal.speak();
            }
        }
    }

    pub fn deinit(self: AnimalUpdate, allocator: std.mem.Allocator) !void{
        for(0..available_types.len) |type_index|{
            const animals = self.get(available_types[type_index]);
            animals.deinit(allocator);
        }
    }
};

when trying to compile the code i get an error saying that the indexes i am trying to use in “update” “deinit” and “init” are runtime, while the “available_types” array is comptime, i also cant just use the index from the loop because then i could only get a “*anyopaque” so no type information which is something i need. i know i could just use a switch similar to one in the “get” function but use a number instead of type, but i would like to not use such a switch either way, because it would be horrible to work with it if i had many types, which is likely.

There are multiple ways to handle this. What the best way is depends on your exact use case. Since you seem to want a separate ArrayList per type, one way is to store those arraylists as fields in the AnimalUpdate struct and use inline for loops to iterate over them:

const AnimalUpdate = struct {
	const available_types = [_]type{Dog, Cat};
	
	dogs: std.ArrayList(Dog),
	cats: std.ArrayList(Cat),
	
	pub fn init() AnimalUpdate {
		var animal_update: AnimalUpdate = undefined;
		
		inline for (available_types) |T|{
			animal_update.get(T).* = .empty;
		}  
		return animal_update;
	}
	
	pub fn get(self: *AnimalUpdate, comptime T: type) *std.ArrayList(T){
		const struct_info = @typeInfo(AnimalUpdate).@"struct";
		inline for (struct_info.fields) |field| {
			if (std.ArrayList(T) == field.type) {
				return &@field(self, field.name);
			}
		}
		@compileError("Unsupported type");
	}
	
	pub fn update(self: *AnimalUpdate) void {
		inline for (available_types) |T| {
			const animals = self.get(T);
			for (animals.items) |animal| {
				animal.speak();
			}
		}
	}
	
	pub fn deinit(self: *AnimalUpdate, allocator: std.mem.Allocator) !void {
		inline for (available_types) |T| {
			const animals = self.get(T);
			animals.deinit(allocator);
		}
	}
};

Other ways that are also possible:

  • Define an Animal struct as tagged union of the possible types. Store 1 list that contains these animals.
  • Use a runtime interface that has a type erased pointer (*anyopaque) and a function pointer to the speak or update function of that type. This is useful if the set of types in not known.

Thanks for help, but this isnt really an ideal solution, the two types are here just for an example, im probably going to use this for possibly tens of different types, so putting the ArrayList of each type as a separate field isnt a great solution. I also dont want to store any runtime information about types. so thanks for the reply but this isnt an ideal solution for my problem