Map with different value types

I want to make a map that can store pointers to any type. Is it possible?
For example:

import "core:fmt"

CHUNK_SIZE :: 100

Position :: struct {
	x, y : int
}

Center :: struct {
	cx, cy: int
}

Component :: union {
	Position,
	Center
}

main :: proc() {
	pos_type : typeid = typeid_of(Position)
	center_type : typeid = typeid_of(Center)
	
	types := new(map[typeid]^[CHUNK_SIZE]Component)
	
	types[typeid_of(Position)] = new([CHUNK_SIZE]Component)
	types[typeid_of(Center)] = new([CHUNK_SIZE]Component)

	positions := types[pos_type]
	centers := types[center_type]

	positions[0] = Position { 10, 20 }
	centers[0] = Center { 5, 7 }

	free(types[pos_type])
	free(types[center_type])
	free(types)
}

It works, but it will waste a memory.
If I could to use something like ^any instead of Component…
Or maybe point what exactly union type should be used when allocate with new…
But from the other perspective if not to make big components and keep them small it will be faster to allocate all chunk of unions instead of separate allocation of each component and storing the pointer in the map…

You most likely don’t want to use any because any internally is just a rawptr and a typeid which means that it’s very-very easy to shoot yourself in the foot. Like storing a pointer to something on the stack…

The “better” approach is to store an opaque type in the map, like Chunk :: []byte which you can allocate just large enough buffer to store what you need.

import "core:slice"

CHUNK_SIZE :: 100
Chunk      :: []byte

new_chunk :: proc($T: typeid) -> Chunk {
	return make(Chunk, size_of(T) * CHUNK_SIZE)
}

Type_Map :: map[typeid]Chunk
Position :: distinct [2]int
Center   :: distinct [2]int

chunk_of :: proc($T: typeid, m: Type_Map) -> []T {
	bytes := m[T]
	return slice.reinterpret([]T, bytes)
}

main :: proc() {
	tm := make(Type_Map)
	tm[Position] = new_chunk(Position)
	tm[Center] = new_chunk(Center)

	positions := chunk_of(Position, tm)
	centers := chunk_of(Center, tm)
}

Honestly, if you have a fix set of components, I’d just drop the map as a whole… Access is faster than a map, heap allocation is 1 single big allocation. Additionally, you could mess around with paddings to make sure arrays are cacheline aligned (if you really need to). It’s also brain dead simple…

Components :: struct {
	positions: [CHUNK_SIZE]Position,
	centers:   [CHUNK_SIZE]Center,
}

Note: you barely have to define stuff like Position as a struct. Use distinct [2]int if you want a named type. Arrays have .x, .y, .z and .w (and even .r, .g, .b, .a) accessors, you can do element-wise operations on them with regular operators like +. They also support swizzling ( e.g. a.xy = a.yx to swap coords).

3 Likes

Thank you! I have to learn more in details before starting ecs… I will try your approach.

1 Like

@zen3ger, sorry for disturbing again.
I am just curious about what of the polymorphism in your code above and, for example, from package base:intrinsics are run at compile time and what at runtime?

I suggest that slice.reinterpret([]T, bytes) should run at runtime.

And, in general, how much using polymorphic code and some intrinsics operation will slow down the result code?

Using fat struct with all components chunks is great for one allocation per chunk/growing instead of allocations equals components count. But it affects only when this allocation happens and in my model it will not so often. But, I need also made some switch/case blocks each time to get access/add/remove components and all these make my code not reusable or, at least, I need to change all places where Components needs to be proceeded.
And adding every new component will lead to changes in all places with switch/case.

Sorry for this topic turned into ecs development discussion.

I want to know only how much using polymorphic code and some intrinsics operation will slow down the result code?

You can drop it in new_chunk but then you need to get the size from Type_Info.

new_chunk :: proc(typ: typeid) -> Chunk {
    //            ^--- is now not a compile time known, concrete type
    //            when called as new_chunk(Position)
    //            size_of(typ) != size_of(Position)
    //            but
    //            size_of(typ) == size_of(typeid)
    info := type_info_of(typ)
    // therefore you need to get the size from runtime type info
    return make(Chunk, info.size * CHUNK_SIZE);
}

But you cannot drop it in chunk_of, because both the type used in casting and the return type has to be a known at compile-time. When T is used (after declared as $T) it behaves like a known, concrete type.

|         | value    | type     |
+---------+----------+----------+
| x: int  | runtime  | concrete |
| $x: int | constant | concrete |
| x: $T   | runtime  | parapoly |
| $x: $T  | constant | parapoly |

In all cases $ is just roughly “copy-paste the code and replace the value at compile-time”. On the type, the diff between parapoly and concrete is just “do I know the type by reading the signature or not”.

Some stuff does happen at runtime, but It needs the $T: typeid. As stated above, casting requires a concrete type. What happens at runtime is the recalculation of the slice length.

Majority of them is runtime. intrinsics.concatenate and intrinsics.constant_log2 are not. If in doubt you can always just try saving the result to a const, e.g. X :: intrinsics.constant_log2(32) // X :: 5.


Parapoly happens at compile time, it rarely has a runtime cost. If in doubt, measure it.

inc :: proc(x: $T) -> T { return x + 1 }
// would be same as writing a specific proc for each type:
// inc(cast(u8)0)    --> inc_u8(0)
// inc(cast(f32)0.0) --> inc_f32(0)

Generally, think of intrinsics are just yet another procedure, only they are internally defined by the compiler.


No matter the approach, at one point you either have to explicitly cast to a type if the returned value is untyped/opaque or have to do a switch to match on the type variant.

Do you have a specific piece of code that’s problematic?

1 Like

I am only designing it in my head and think how to implement in odin.
I come to result that it will be more simple and efficient to create dead simple struct with component chunks.

Just one file with custom code that will ne changed from game to game. Other api can be universal. Maybe not so professional but simple and effective. Odin was designed for such things, it’s not rust, less abstraction more joy of programming.