The most common reason for this that I see from an ECS perspective is to avoid invalidating pointers on subsequent insertions to the vector sequence. Components will often want to point to each other (they could do it via indices rather than pointers, although it's a bit less convenient in code). For example, motion parenting as needed to do FK/IK will want components to store some sort of link (pointer or index) to child components to link them together (assuming you don't use a completely separate tree structure living outside your ECS). You can make up for the heap allocation overhead of operator new/delete
with an efficient allocator using memory pools, like a freelist with constant-time allocations/deallocations.
Personally, my performance requirements are so tight though that I don't do this. Our ECS can often have millions of entities inside, and it helps to use 32-bit indices here rather than 64-bit pointers on 64-bit architectures and avoid the extra heap allocations (even using memory pools). We use the ECS on the server-side for an MMO with a massive world. But if it's like 10k entities rather than millions, I think using a sequence of pointers here, dynamically allocated, is more productive than integers storing a relative index to the vector. Ours has the programming burden of having to have access to the ECS scene together with the index to get to a particular component from a source component referencing it with an array lookup rather than direct pointer access but it was a productivity compromise we found was worth it after measuring before and after.
But the number one reason I see to do this is to avoid invalidating pointers as std::vector<T>
will do on insertions if T
is not a pointer. I don't think polymorphism typically applies here with a need to store base pointers in an ECS like some base Component
type that components inherit with overrides to virtual functions since an ECS generally stores and fetches (casts) component types directly of a particular type. If polymorphism is used here to make the code a bit more type-safe, then it's typically applied on a component container level (i.e, ComponentList
base type) rather than per component in ones at least slightly concerned with efficiency. I could see the ability to move pointers around as a valid reason if the ECS is designed such that it shifts the contents of its containers around for more efficient component queries, although it makes less sense to me in archetype systems that fundamentally want to avoid even the inefficiencies of using variable strides jumping all over memory to iterate through components of multiple types (something which can still incur many cache misses even if you sort the pointers in advance by address for multi-component-type sequential access patterns). They usually want to minimize even basic cache misses with multi-type component queries so they usually rearrange and cache memory at the pointee level and not just pointer/index level to optimize queries.
So I think the most common reason here is to avoid pointer invalidation on insertions, which ultimately translates to programmer convenience. At least after implementing multiple ECS architectures and looking at a bunch out there, that would be the number one reason I'd see to do this as a productivity compromise at some cost to efficiency (which might not be a big cost in some game that only has, say, 10k entities on average in a given scene... although it might not even be worth bothering to implement archetypes in those).