-
-
Notifications
You must be signed in to change notification settings - Fork 897
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Component polymorphism support #871
base: wip
Are you sure you want to change the base?
Conversation
Codecov ReportAll modified and coverable lines are covered by tests ✅
❗ Your organization needs to install the Codecov GitHub app to enable full functionality. Additional details and impacted files@@ Coverage Diff @@
## wip #871 +/- ##
==========================================
Coverage 100.00% 100.00%
==========================================
Files 129 134 +5
Lines 19539 20019 +480
==========================================
+ Hits 19539 20019 +480 ☔ View full report in Codecov by Sentry. |
Updated the base branch to |
Thank you, I've reproduced this one on my machine, and I will commit the fix tomorrow |
It seems, the problem goes much deeper, than just a bug with parameter pack expansion in static assert. It looks like there are some problems with Here, when if constexpr(std::is_pointer_v<Type>) {
// Type and ChildType are pointers, no need for getting address
ChildType ptr = static_cast<StorageType*>(pool)->get(entity);
return static_cast<Type>(ptr);
} else {
// Type is a base of ChildType, do pointer conversion
return static_cast<Type*>(std::addressof(static_cast<StorageType*>(pool)->get(entity)));
} However, this code compiles successfully, both for template<typename T>
void test_if_constexpr(T a, T b) {
if constexpr(std::is_pointer_v<T>)
*a = *b;
else
a = a + b;
} Maybe you've encountered something like this and got some ideas about the solution to this problem? The only solution I see right now is just not using |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
First pass to leave some comments and questions before proceeding.
I need to review the whole thing again for sure, mainly because there are some parts that aren't that clear to me yet.
I really appreciate the effort to make it an external tool.
However, if I get this as the bare minimum, I see lot of code here and I wonder whether it's necessary and if we can reduce and simplify it.
Unfortunately I'm not (yet) into all the details, so I can't say where and how to intervene. My gut feeling is that we can shrink some stuff probably but I might be utterly wrong.
In any case, get ready for a second pass soon, according with my schedule. 👍
|
||
private: | ||
template<typename... ParentTypes> | ||
void bind_all_parent_types(basic_registry<entity_type>& reg, [[maybe_unused]] type_list<ParentTypes...>) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is contrived imho. You expect a user to want to bind all parent types but, in fact, trying to automate this as much as possible takes away the user's ability to choose.
I'm not sure this is the right way to go. The whole library is also designed on the opposite principle.
Without changing anything yet, can you explain what the burden is on the user in the other case?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
User can directly declare, what types are parent to the current component by specializing entt::direct_parent_types
or entt::parent_types
. These two provide complete freedom in declaring parent type list for each separate type.
So having control over this particular method only provides the ability to bind parent list, different from the one, that is already declared. However, the declared one is currently used only in this method and a bunch of static asserts, and I'm not sure, that modifying list in this method could be useful.
src/entt/entity/polymorphic.hpp
Outdated
template<typename Entity> | ||
class poly_pool_holder_base { | ||
public: | ||
inline poly_pool_holder_base(basic_sparse_set<Entity>* pool, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This ignores deliberately the allocator of a sparse set and can easily break if it's different from the default one.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see, gonna think how to fix this. Thanks for pointing this out.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems to be a really nasty problem here. I may be wrong, but because everything within basic_sparse_set
depends on a pointer type, provided by the allocator, it is required to virtualize everything, even the iteration itself, which is not viable. Maybe you've got some ideas how to avoid this?
My only thought is just banning using different allocators within the hierarchy. This can possibly be reduced to restricting using allocators with different pointer types, because the we don't directly allocate or deallocate anything, I just haven't examined it that closely at the moment. This one also does not look particularly good, but at least it is zero cost in terms of performance.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you just turn this:
template<typename Entity>
class poly_pool_holder_base
Into this:
template<typename Storage>
class poly_pool_holder_base
Then use like typename Storage::entity_type
for your purposes? Dunno if it works but the runtime view does something similar to abstract the sparse set.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for your advice. I finally got my hands on looking through runtime_view
implementation. As I see, it allows to add only storages with the same allocator type. This seems to be similar to my proposal in the comment above, so I will try to implement it and see, how it works. I've also refined the idea a bit, so here it is.
In short, we restrict using pools with different pointer to entity types in the same hierarchy (the pointer type is the only thing we get from allocator, that is used in iteration/contains checks). Then we use allocator type, we got from storage_traits
. We can do this at compile time for all parent types, or at runtime, only for types, which bind child pools to the poly_type
.
In the compile time case, we check, that each parent of a poly type uses allocator with the same pointer type, that's all. In the runtime case the poly_type<entity_type, value_type>
knows the allocator type of the storage for the value_type
from storage_traits
, so then we just assert type ids are equal for all bound child pools.
I will probably do a compile time variant first, as it seems more reliable and adds no cost. But runtime one has one advantage - it does not check allocator for components, that were never added.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've implemented the compile time variant, that was mentioned above, however, there are some changes. So right now it looks something like this:
- I've added
entt::poly_type_allocator<Type>
to specialize allocator type for polymorpic component type, by default it usesstd::allocator
. This is only used as default allocator type getter, and in each place, where it is used, user can change it. - The default specialization of
entt::type_traits
for polymorphic components now usesentt::poly_type_allocator
to pass allocator type to storage. entt::poly_type<Entity, Type>
now has new template parameter -Allocator
, and so doesentt::poly_type_holder
, so now they operate on pools using the correct allocator type. When child pool is bound toentt::poly_type
, it ensures (viastatic_assert
), that pointer-to-entity type from the bound pool and from its own allocator type are same.- When bound as a child pool, the default implementation of
entt::poly_storage_mixin
usesentt::poly_type_allocator
to provide allocator parameter toentt::poly_type
, however this can be changed in user defined mixins. entt::poly_types_accessor
specializations now must declareallocator_type<T>
type meta function, to get the allocator type for the given polymorphic component type. This is required forentt::assure_poly_type
, which is used in every polymorphic algorithm (and, most likely, in user-defined ones), to pass the allocator type to the accessedentt::poly_type
. The default specialization ofentt::poly_type_accessor
forentt::basic_registry
, yet again, usesentt::poly_type_allocator
for this, but for custom specializations, it gives user complete control over allocator types.
template<typename... ParentTypes> | ||
void bind_all_parent_types(basic_registry<entity_type>& reg, [[maybe_unused]] type_list<ParentTypes...>) { | ||
(reg.ctx().template emplace<poly_type<entity_type, ParentTypes>>().bind_child_storage(this), ...); | ||
reg.ctx().template emplace<poly_type<entity_type, value_type>>().bind_child_storage(this); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the purpose or use case of this? Not sure I get it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure what do you mean here exactly, sorry. Are you asking, why I'm binding child storage for the value_type
here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, sorry, my fault. I meant the value_type
part indeed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is done because when we are iterating through components, derived from Type
, the instances of Type
itself must be among them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mmm is it? This is another assumption on which I don't have control as an user and that I don't see as mandatory necessarily. 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For your example let's assume, that there is another class, derived from circle
. I want to iterate each circle
and anything that derives from it, so I choose to use poly_each
.
To do this, I get poly_type<entity, circle>
and iterate every pool, that was bound as child to it. For this, to include instances of circle
itself, the circle
pool must be bound as a child pool for circle
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doesn't it confirm that there are multiple use cases and no choice? 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I'm missing your idea here, sorry. You mean the use case, when user wants to iterate over all components, derived from circle
, but ignore all instances of circle
itself at the same time? Am I correct here?
I mean, it is natural with abstract base class like shape
, that could not be instantiated by itself, and I would want only its derived types. But in this case there just will be no pool for shape
, so everything is ok. But for the case with circle
I can't see any useful application of such behavior.
But I feel, that you've meant something else. If it is so, please give me examples of different possible use cases you have in mind, I will greatly appreciate it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, sorry, I was out for the weekend and I was in a rush while trying to catch up with all the other things today.
Imagine you have shape
(the base), circle
and rectangle
. What I end up after a while is:
poly_type<entity_type, shape>>
poly_type<entity_type, circle>>
poly_type<entity_type, rectangle>>
What I don't get is why the last two exist and what their purpose is. They don't have derived classes (let's make them final
) and all I want is to iterate my shapes. Therefore, I can see the reason for the first poly_type
while the others look useless.
Does it make sense now?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, I think I understand now. To be sure: you are concerned, that while some poly types don't have any children and could be treated as a non-polymorphic ones, they still put poly_type
for themselves into the context, and internally it allocates a vector of at least one element, so we still pay for having them?
Well, at the compile time we cannot be sure, that shape
and circle
don't have any children, if they aren't declared final. They may have derived types or they may not, we cannot tell this. Having poly_type
allows us to iterate every pool, containing this type, it will work fine with or without any derived types. So having poly_type
for each polymorphic type is very convenient, and, moreover, it is intuitive to have it for all types.
Of course, at runtime, we can add poly_type
only for types, that have children, and then in each algorithm we can check, if we have a poly_type
for given type, and, if we don't, fallback to non-polymorphic algorithm. But this solution breaks a really nice abstraction, that does not require polymorphic type data to be stored in the registry, because we will be required to use registry in the fallback case anyway. And the algorithms will become bigger and uglier, all of this for no reasonable gain.
However, I think I know how to resolve this. Instead of binding storage as a child for its own poly_type
, which will internally cause heap allocation by the vector, we can store it separately in some, for example, std::optional<poly_type_holder> self_type_holder
. In this case we still pay for polymorphic type with no children, compared to non-polymorphic type, but this cost is much smaller, than before, and anything else is not broken. What do you think?
* @brief Used to inherit from all given parent types and declare inheriting type polymorphic with given direct parents. | ||
* All parent types are required to be polymorphic | ||
* @code{.cpp} | ||
* struct A : public entt::inherit<> {}; // base polymorphic type with no parents |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
At the end of the day, why I as an user should decide to inherit from a third party library class (and thus pollute my types and hierarchies) if you also give me an alternative? That is, is inherit
really required or just syntactic sugar?
I can't yet wrap my mind around of it but it looks like one can just make it work without inheriting from it and this would make the whole thing more natural in a sense:
struct derived: base { /* definition */ };
This is my type, part of my application, it tells me about my intention and purpose. Why one should turn it into something like:
struct derived: entt::inherit<base> { /* definition */ };
I don't really see any reason for which I would do that as a final user. Granted, I'm a huge fan of non-invasive designe, solutions, whatever but still, I can accept an invasive approach if there is a reason and I don't understand the benefit here. Therefore I'm a bit puzzled.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
entt::inherit
is just a shorter alternative for using direct_parent_types = entt::type_list<Parent1, Parent2, ...>
or separate declaration with entt::direct_parent_types
, so yes, it is just syntactic sugar. But for me personally, it just looks a lot nicer, so I would like to keep it just as one of several ways of declaring hierarchy.
src/entt/entity/polymorphic.hpp
Outdated
namespace entt::internal { | ||
|
||
template<typename Convert, typename It> | ||
struct converting_iterator { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What does this iterator return? What's the use case? I can't easily see it at a first glance, sorry.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This iterator is used in algorithms, when iterating through internal vector of poly_pool_holder_base
inside the poly_type
and static casting it to the typed version. However, it was used before refactoring the whole thing and now I see the much shorter solution, by just using lambda instead of iterator. So it is to be removed, and thank you for making me notice it is still there.
src/entt/entity/polymorphic.hpp
Outdated
namespace entt::internal { | ||
|
||
template<typename Convert, typename It> | ||
struct converting_iterator { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What does this iterator return? What's the use case? I can't easily see it at a first glance, sorry.
|
||
|
||
template<typename PoolsIterator> | ||
class poly_components_iterator { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as above. Thanks.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This one is used to iterate over all components for one entity, by iterating over all derived component pools and skipping ones, where the entity is not present. It is used in poly_get_all
and poly_get_any
.
* @param ent entity | ||
* @return true, if component was removed, false, if it didnt exist | ||
*/ | ||
inline bool remove(const Entity ent) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can't find where you fill the remover_ptr
but I still feel like you don't really need to.
You can delete an entity from a pointer to a sparse set. No need to wrap that functionality probably?
That is, the following should work just fine afaik:
pool_ptr->remove(ent); // or erase too, not sure what the purpose is here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see, I haven't noticed that the method called by erase
is virtual.
Thank you for reviewing, I greatly appreciate your effort. I've replied to the comments above, and uploaded quick fixes for some of them. However there are still some things to be done and to discuss, so I am working on it. Looking forward for the second pass :) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A few comments but it's also half past midnight, so let's settle for now. 😅
template<typename... ParentTypes> | ||
void bind_all_parent_types(basic_registry<entity_type>& reg, [[maybe_unused]] type_list<ParentTypes...>) { | ||
(reg.ctx().template emplace<poly_type<entity_type, ParentTypes>>().bind_child_storage(this), ...); | ||
reg.ctx().template emplace<poly_type<entity_type, value_type>>().bind_child_storage(this); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, sorry, I was out for the weekend and I was in a rush while trying to catch up with all the other things today.
Imagine you have shape
(the base), circle
and rectangle
. What I end up after a while is:
poly_type<entity_type, shape>>
poly_type<entity_type, circle>>
poly_type<entity_type, rectangle>>
What I don't get is why the last two exist and what their purpose is. They don't have derived classes (let's make them final
) and all I want is to iterate my shapes. Therefore, I can see the reason for the first poly_type
while the others look useless.
Does it make sense now?
* @return Returns reference to Type, converted from a given pointer. Will return pointer instead of reference, if parent type is a pointer. | ||
*/ | ||
inline pointer_type try_get(const Entity ent) ENTT_NOEXCEPT { | ||
return pointer_type(this->getter_ptr(this->pool_ptr, ent)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The basic sparse set also has an opaque getter that accepts an entity and returns a void *
. Isn't it what you want here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This one handles correct pointer conversion between a pair of child and parent types, so it behaves correctly in case of multiple inheritance. It also handles conversion for pointer-like polymorphic components. Inside, getter_ptr
calls non-virtual basic_storage::get
to avoid double virtual call.
src/entt/entity/polymorphic.hpp
Outdated
|
||
/** @copydoc try_get */ | ||
inline const_pointer_type try_get(const Entity ent) const ENTT_NOEXCEPT { | ||
return const_cast<poly_pool_holder<Entity, Type>*>(this)->try_get(ent); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as above.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just a couple of comments. The best I can do on Sunday. Sorry. 😅
My only concern is that I feel this is getting pretty big for the feature it wants to introduce. Therefore, I want to get some time to see why and where this is happening so as to reduce it to something acceptable.
My gut feeling is that it's partly due to trying to tackle some corner cases for which not compiling would already be enough but take this with a grain of salt. 👍
* @tparam Type polymorphic component type to convert into | ||
* @tparam Allocator allocator type of the pool | ||
*/ | ||
template<typename Entity, typename Type, typename Allocator> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't this the same as using type Storage
, then taking typename Storage::entity_type
and the others (ie value_type
and allocator_type
) from it?
It would also vastly simplify the definition at a first glance.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, but poly_type
has to be created for types, that could not have storage type (most likely ones with pure virtual methods). The possible workaround here, that I see right away, is to create storage_traits
specialization for polymorphic types, that cannot have a storage type, with some placeholder storage type, that only provides entity_type
, value_type
and allocator_type
. Then, the storage_traits
could be used to access polymorphic types by pair of entity and component type.
But it looks like, this could possibly break something in the abstraction of polymorphic algorithms from registry, that I cannot see right away. Also, this will not simplify things for the end user at all, because he would still be required to declare custom allocator type in any case. And either it will not make life a lot simpler inside the implementation, because all types for poly_type
, except the component type itself, is acquired from the poly_types_accessor
anyway.
* @tparam Type polymorphic component type | ||
* @tparam Allocator allocator type for all bound contained pool holders | ||
*/ | ||
template<typename Entity, typename Type, typename Allocator> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is similar to poly_pool_holder
and I wonder why we don't get the storage type directly rather than a bunch of things that can quickly go out of sync.
Some of the builds have failed again, I will try reproduce it and commit the fix, as it will be ready, sorry. |
Hello, I'm very sorry for disappearing, I unfortunately had a very little free time. However now I am ready to continue the discussion and further improvement. Everything seems to build properly, but codecov now does something strange, both in |
The one in |
I have also noticed, while testing stuff, that it has no support for calling the base component class with parameters. Am I missing something? Is there not a way to do that currently? |
Hello, and sorry for a late reply! I'm really glad that you've tried and tested my extension to
I see that you are using my first prototype, which was considered too complicated and introduced too many changes. Currently discussed implementation is located in the The old implementation, that you've used, internally allocates blocks of memory to store tightly packed reference lists, those are stored statically, and it seems, that I've forgot to free them, when program terminates. This is the most probable source of this leak.
Sorry, I'm not getting the idea here, can you please provide an example? |
b1b44fb
to
5db8ad5
Compare
d53323e
to
89166f0
Compare
38e1467
to
b1e47fc
Compare
25fe7cc
to
5080289
Compare
5a2da65
to
eccac01
Compare
Adds extension, that allows to declare and work with polymorphic components. See discussion in the delated issue #859.
Example usage (will be updated in case of changes):