But for anything else, handles can be trouble.
First, you're obviously paying indirection tax on EVERY lookup. Logic like `x->foo` becomes `memory_allocator.objects[x->index]->foo`. You're also paying the generation counter tax (assuming you want any kind of safety). So you also have to do something like this:
if (x->index < 0 || x->index > memory_allocator.objects.size()) { // panic? }
auto *ref = memory_allocator.objects[x->index];
if (!ref) { // panic? }
if (ref->generation != x->generation) { /// panic? }
return *ref;
Next, you have type bloat. Hopefully you are making type-safe versions of all these indices so you can't just use an aribtrary index into any kind of array right? Ok well that means now you have double the types (one index type per pointer type). Ok maybe not the worst, just a lot of boilerplate everywhere. Prepare for a lot of this: struct Handle { uint64_t index; uint64_t generation }
typedef PlayerHandle Handle;
Handles are also a lot bigger. If you're memory constrained, adding an additional 16-32 bytes per pointer allocation is painful. For my programming lang I have a single test file that contains 300K AST nodes, that would be another 9.6MB to parse a single file for handles alone, for noticeably slower performance (300K+ indirections and cache misses).Next, you have the issue of resizing your memory array. You now are in "write your own garbage collector" land. You want to consolidate 50% of the memory that is suddenly free? Well you need to change every index to point the new compacted range (expensive and complex), or manage a free list which potentially increases the memory requirement, and adds complexity, cache coherence issues, etc. Do your memory lists grow indefinitely? Do you need to think about having separate lists for short lived and long lived (generational GC style)? Managing lifespan of objects all sharing one backing storage when the indexes are just integers strewn about is actually _harder_ than doing the same with pointers (just use compacting GC already).
Then there's the issue of object pools. You want your data to live in arrays. Do you put all objects into one master array? You lose cache coherence / iteration speed, and type safety (unless you really bloat and start adding type info to every entry). If you use different arrays for each you might end up with dozens of arrays, one per type, and also their free lists. Now you need a way to manage the lifespans and scope of all these object pools. Suddenly, you've reinvented malloc / free and segfault signals.
Or you could just wisely use pointers...
[–] Panzerschrek 4 days ago
[–] program_whiz 4 days ago
Same for other responses. Great, you can just implement a lifespan array in every subsystem. If your sub systems are very simple (e.g. single Entity component is the only thing you're allocating in a game, maybe Resource as well), then sure, its simple. Not sure what it's buying you, since you apparently are also freeing all entities and scene elements at once too. But for a complex system where there are many types of objects (dozens or hundreds), and where lifespans are complex (some short, some long), then having "every system implement its own pool" adds a lot of complexity. Then not only would you want type-safety with a Handle<Object> for every type of object, but also some kind of affinity for the sub-system which currently manages that lifespan (unless you just have a single Pool for every object type, which re-introduces the lifespan issues).
Again, other responses I'll lump into "sounds like for simple systems it isn't substantially more difficult than pointers". I guess if it has a meaningful performance tradeoff, then it might be worth it (where a simple free list or using system malloc would be too expensive).
If you say you don't need many objects, so counters can be small, and you never need to resize the handles array, so indexes are valid, and you don't have lots of object types, and don't have lots of subsystems, and lifespans are purely scoped to subsystem lifespans, and don't mind type bloat all over your interfaces, then it sounds like handles are a perfect fit. I'm trying to imagine what's so bad about using pointers in such a simple system, and what we're "solving" (e.g. just &memory_array[next_index++]).
It's not true for typical use-cases, when objects aren't processed one by one, but in bulk.
> Next, you have type bloat
What's the problem? Do you pay a special tax each time you need create a new type?
> Prepare for a lot of this
That's not how it's done in C++. You typically have a template for handle types, so that PlayerHandle is just Handle<Player>.
> Handles are also a lot bigger.
The article mentions, that handles are typically smaller than pointers. A pointer is 8 bytes (on 64-bit machines), but handles can be smaller if not so many objects are needed to be created.
> Next, you have the issue of resizing your memory array.
There is only problematic to resize it down, which is almost never needed.
> dozens of arrays
> manage the lifespans and scope of all these object pools
It's not a problem. Each subsystem owns its arrays and destroys them only when the subsystem itself is destroyed.