review: instance-less pools for empty types

This commit is contained in:
Michele Caini
2019-05-10 10:44:01 +02:00
parent 91fac8049c
commit f3eaeb96f0
11 changed files with 86 additions and 116 deletions

3
TODO
View File

@@ -22,4 +22,7 @@
* early out in views using bitmasks with bloom filter like access based on modulus
- standard each, use bitmask to speed up the whole thing and avoid accessing the pools to test for the page
- iterator based each with a couple of iterators passed from outside (use bitmask + has)
* multi component registry::remove and some others?
* reactive systems
* update doc + inline doc (instance-less pools for empty types)

View File

@@ -1460,25 +1460,21 @@ groups or as free types with multi component views and groups in general.
An empty type `T` is such that `std::is_empty_v<T>` returns true. They are also
the same types for which _empty base optimization_ (EBO) is possibile.<br/>
`EnTT` handles these types in a special way, optimizing both in terms of
performance and memory usage. However, this also has drawbacks that are worth
performance and memory usage. However, this also has consequences that are worth
mentioning.
When an empty type is detected, a pool is created in such a way that one and
only one instance of the given type is created. All the entities will refer to
it. Since the type is, in fact, empty, this is safe and there is no risk that a
modification of its data members will affect other instances.<br/>
Iterations are faster because only the entities to which the type is assigned
are considered. Similarly, less memory is used, since there exists always only
one instance of the component itself, no matter how many entities it is
assigned.
When an empty type is detected, it's not instantiated in any case. Therefore,
only the entities to which it's assigned are made availble. All the iterators as
well as the `get` member functions of registries, views and groups will return
temporary objects. Similarly, some functions such as `try_get` or the raw access
to the list of components aren't available for this kind of types.<br/>
On the other hand, iterations are faster because only the entities to which the
type is assigned are considered. Moreover, less memory is used, since there
doesn't exist any instance of the component, no matter how many entities it is
assigned to.
The drawback is that the `raw` member function will no longer be able to return
a valid pointer to the list of components in the pool. This is because there is
no list of components at all. Only one instance of the given type exists in this
case. Therefore, `raw` will always return a pointer to that instance.<br/>
Nonetheless, the iterators returned by the `begin` and `end` member functions
are still valid and can be used safely. More in general, all the features
offered by the library aren't affected, but for the `raw` member function.
More in general, none of the features offered by the library is affected, but
for the ones that require to return actual instances.
# Multithreading

View File

@@ -92,7 +92,7 @@ struct basic_actor {
* @return A reference to the newly created component.
*/
template<typename Component, typename... Args>
Component & assign(Args &&... args) {
decltype(auto) assign(Args &&... args) {
return reg->template assign_or_replace<Component>(entt, std::forward<Args>(args)...);
}

View File

@@ -164,11 +164,6 @@ public:
* There are no guarantees on the order of the components. Use `begin` and
* `end` if you want to iterate the group in the expected order.
*
* @warning
* Empty components aren't explicitly instantiated. Only one instance of the
* given type is created. Therefore, this function always returns a pointer
* to that instance.
*
* @tparam Component Type of component in which one is interested.
* @return A pointer to the array of components.
*/
@@ -296,14 +291,13 @@ public:
* @return The components assigned to the entity.
*/
template<typename... Component>
std::conditional_t<sizeof...(Component) == 1, std::tuple_element_t<0, std::tuple<Component &...>>, std::tuple<Component &...>>
get([[maybe_unused]] const entity_type entt) const ENTT_NOEXCEPT {
decltype(auto) get([[maybe_unused]] const entity_type entt) const ENTT_NOEXCEPT {
ENTT_ASSERT(contains(entt));
if constexpr(sizeof...(Component) == 1) {
return (std::get<pool_type<Component> *>(pools)->get(entt), ...);
} else {
return std::tuple<Component &...>{get<Component>(entt)...};
return std::tuple<decltype(get<Component>(entt))...>{get<Component>(entt)...};
}
}
@@ -322,13 +316,18 @@ public:
* void(Get &...);
* @endcode
*
* @note
* Empty types aren't explicitly instantiated. Therefore, temporary objects
* are returned during iterations. They can be caught only by copy or with
* const references.
*
* @tparam Func Type of the function object to invoke.
* @param func A valid function object.
*/
template<typename Func>
inline void each(Func func) const {
for(const auto entt: *handler) {
if constexpr(std::is_invocable_v<Func, std::add_lvalue_reference_t<Get>...>) {
if constexpr(std::is_invocable_v<Func, decltype(get<Get>({}))...>) {
func(std::get<pool_type<Get> *>(pools)->get(entt)...);
} else {
func(entt, std::get<pool_type<Get> *>(pools)->get(entt)...);
@@ -422,14 +421,30 @@ class basic_group<Entity, get_t<Get...>, Owned...> {
using component_iterator_type = decltype(std::declval<pool_type<Component>>().begin());
template<typename Component>
const Component & from_index(const typename sparse_set<Entity>::size_type index) {
decltype(auto) from_index(const typename sparse_set<Entity>::size_type index) {
if constexpr(std::disjunction_v<std::is_same<Component, Owned>...>) {
return std::get<pool_type<Component> *>(pools)->raw()[index];
if constexpr(std::is_empty_v<Component>) {
return std::get<pool_type<Component> *>(pools).get();
} else {
return std::as_const(*std::get<pool_type<Component> *>(pools)).raw()[index];
}
} else {
return std::get<pool_type<Component> *>(pools)->get(data()[index]);
return std::as_const(*std::get<pool_type<Component> *>(pools)).get(data()[index]);
}
}
template<typename Component>
inline auto swap(int, pool_type<Component> *cpool, const std::size_t lhs, const std::size_t rhs)
-> decltype(cpool->raw(), void()) {
std::swap(cpool->raw()[lhs], cpool->raw()[rhs]);
cpool->swap(lhs, rhs);
}
template<typename Component>
inline void swap(char, pool_type<Component> *cpool, const std::size_t lhs, const std::size_t rhs) {
cpool->swap(lhs, rhs);
}
// we could use pool_type<Type> *..., but vs complains about it and refuses to compile for unknown reasons (likely a bug)
basic_group(const typename basic_registry<Entity>::size_type *sz, storage<Entity, std::remove_const_t<Owned>> *... owned, storage<Entity, std::remove_const_t<Get>> *... get) ENTT_NOEXCEPT
: length{sz},
@@ -495,11 +510,6 @@ public:
* There are no guarantees on the order of the components. Use `begin` and
* `end` if you want to iterate the group in the expected order.
*
* @warning
* Empty components aren't explicitly instantiated. Only one instance of the
* given type is created. Therefore, this function always returns a pointer
* to that instance.
*
* @tparam Component Type of component in which one is interested.
* @return A pointer to the array of components.
*/
@@ -630,14 +640,13 @@ public:
* @return The components assigned to the entity.
*/
template<typename... Component>
std::conditional_t<sizeof...(Component) == 1, std::tuple_element_t<0, std::tuple<Component &...>>, std::tuple<Component &...>>
get([[maybe_unused]] const entity_type entt) const ENTT_NOEXCEPT {
decltype(auto) get([[maybe_unused]] const entity_type entt) const ENTT_NOEXCEPT {
ENTT_ASSERT(contains(entt));
if constexpr(sizeof...(Component) == 1) {
return (std::get<pool_type<Component> *>(pools)->get(entt), ...);
} else {
return std::tuple<Component &...>{get<Component>(entt)...};
return std::tuple<decltype(get<Component>(entt))...>{get<Component>(entt)...};
}
}
@@ -656,6 +665,11 @@ public:
* void(Owned &..., Get &...);
* @endcode
*
* @note
* Empty types aren't explicitly instantiated. Therefore, temporary objects
* are returned during iterations. They can be caught only by copy or with
* const references.
*
* @tparam Func Type of the function object to invoke.
* @param func A valid function object.
*/
@@ -665,7 +679,7 @@ public:
[[maybe_unused]] auto data = std::get<0>(pools)->sparse_set<entity_type>::end() - *length;
for(auto next = *length; next; --next) {
if constexpr(std::is_invocable_v<Func, std::add_lvalue_reference_t<Owned>..., std::add_lvalue_reference_t<Get>...>) {
if constexpr(std::is_invocable_v<Func, decltype(get<Owned>({}))..., decltype(get<Get>({}))...>) {
if constexpr(sizeof...(Get) == 0) {
func(*(std::get<component_iterator_type<Owned>>(raw)++)...);
} else {
@@ -747,8 +761,7 @@ public:
while(curr != next) {
const auto lhs = copy[curr];
const auto rhs = copy[next];
(std::swap(std::get<pool_type<Owned> *>(pools)->raw()[lhs], std::get<pool_type<Owned> *>(pools)->raw()[rhs]), ...);
(std::get<pool_type<Owned> *>(pools)->swap(lhs, rhs), ...);
(swap<Owned>(0, std::get<pool_type<Owned> *>(pools), lhs, rhs), ...);
copy[curr] = curr;
curr = next;
next = copy[curr];

View File

@@ -564,8 +564,7 @@ public:
* just created otherwise.
*/
template<typename... Component>
std::conditional_t<sizeof...(Component) == 0, entity_type, std::tuple<entity_type, Component &...>>
create() {
decltype(auto) create() {
entity_type entity;
if(available) {
@@ -584,7 +583,7 @@ public:
if constexpr(sizeof...(Component) == 0) {
return entity;
} else {
return { entity, assign<Component>(entity)... };
return std::tuple<entity_type, decltype(assign<Component>(entity))...>{entity, assign<Component>(entity)...};
}
}
@@ -602,8 +601,7 @@ public:
* sorted the same of the entities otherwise.
*/
template<typename... Component, typename It>
std::conditional_t<sizeof...(Component) == 0, void, std::tuple<Component *...>>
create(It first, It last) {
auto create(It first, It last) {
static_assert(std::is_convertible_v<entity_type, typename std::iterator_traits<It>::value_type>);
const auto length = size_type(std::distance(first, last));
const auto sz = std::min(available, length);
@@ -630,7 +628,7 @@ public:
});
if constexpr(sizeof...(Component) > 0) {
return { assure<Component>()->batch(*this, first, last)... };
return std::make_tuple(assure<Component>()->batch(*this, first, last)...);
}
}
@@ -783,17 +781,19 @@ public:
if constexpr(sizeof...(Component) == 1) {
return (pool<Component>()->get(entity), ...);
} else {
return std::tuple<std::add_const_t<Component> &...>{get<Component>(entity)...};
return std::tuple<decltype(get<Component>(entity))...>{get<Component>(entity)...};
}
}
/*! @copydoc get */
template<typename... Component>
inline decltype(auto) get([[maybe_unused]] const entity_type entity) ENTT_NOEXCEPT {
ENTT_ASSERT(valid(entity));
if constexpr(sizeof...(Component) == 1) {
return (const_cast<Component &>(std::as_const(*this).template get<Component>(entity)), ...);
return (pool<Component>()->get(entity), ...);
} else {
return std::tuple<Component &...>{get<Component>(entity)...};
return std::tuple<decltype(get<Component>(entity))...>{get<Component>(entity)...};
}
}
@@ -825,8 +825,7 @@ public:
decltype(auto) get_or_assign(const entity_type entity, Args &&... args) ENTT_NOEXCEPT {
ENTT_ASSERT(valid(entity));
auto *cpool = assure<Component>();
auto *comp = cpool->try_get(entity);
return comp ? *comp : cpool->assign(*this, entity, std::forward<Args>(args)...);
return cpool->has(entity) ? cpool->get(entity) : cpool->assign(*this, entity, std::forward<Args>(args)...);
}
/**

View File

@@ -620,19 +620,6 @@ public:
/*! @brief Random access iterator type. */
using iterator_type = iterator;
/**
* @brief Direct access to the array of objects.
*
* @note
* Empty types aren't explicitly instantiated. Therefore, this function
* always returns a null pointer.
*
* @return A pointer to the array of objects.
*/
const object_type * raw() const ENTT_NOEXCEPT {
return nullptr;
}
/**
* @brief Returns an iterator to the beginning.
*
@@ -698,21 +685,6 @@ public:
ENTT_ASSERT(underlying_type::has(entt));
return {};
}
/**
* @brief Returns a pointer to the object associated with an entity, if any.
*
* @note
* Empty types aren't explicitly instantiated. Therefore, this function
* always returns a null pointer.
*
* @param entt A valid entity identifier.
* @return The object associated with the entity, if any.
*/
const object_type * try_get([[maybe_unused]] const entity_type entt) const ENTT_NOEXCEPT {
ENTT_ASSERT(underlying_type::has(entt));
return nullptr;
}
};
/*! @copydoc basic_storage */

View File

@@ -179,7 +179,7 @@ class basic_view {
auto curr = raw++;
if((std::get<pool_type<Other> *>(pools)->has(entity) && ...)) {
if constexpr(std::is_invocable_v<Func, std::add_lvalue_reference_t<Component>...>) {
if constexpr(std::is_invocable_v<Func, decltype(get<Component>({}))...>) {
func(get<Comp, Component>(curr, std::get<pool_type<Component> *>(pools), entity)...);
} else {
func(entity, get<Comp, Component>(curr, std::get<pool_type<Component> *>(pools), entity)...);
@@ -244,11 +244,6 @@ public:
* There are no guarantees on the order of the components. Use `begin` and
* `end` if you want to iterate the view in the expected order.
*
* @warning
* Empty components aren't explicitly instantiated. Only one instance of the
* given type is created. Therefore, this function always returns a pointer
* to that instance.
*
* @tparam Comp Type of component in which one is interested.
* @return A pointer to the array of components.
*/
@@ -354,14 +349,13 @@ public:
* @return The components assigned to the entity.
*/
template<typename... Comp>
std::conditional_t<sizeof...(Comp) == 1, std::tuple_element_t<0, std::tuple<Comp &...>>, std::tuple<Comp &...>>
get([[maybe_unused]] const entity_type entt) const ENTT_NOEXCEPT {
decltype(auto) get([[maybe_unused]] const entity_type entt) const ENTT_NOEXCEPT {
ENTT_ASSERT(contains(entt));
if constexpr(sizeof...(Comp) == 1) {
return (std::get<pool_type<Comp> *>(pools)->get(entt), ...);
} else {
return std::tuple<Comp &...>{get<Comp>(entt)...};
return std::tuple<decltype(get<Comp>(entt))...>{get<Comp>(entt)...};
}
}
@@ -380,6 +374,11 @@ public:
* void(Component &...);
* @endcode
*
* @note
* Empty types aren't explicitly instantiated. Therefore, temporary objects
* are returned during iterations. They can be caught only by copy or with
* const references.
*
* @tparam Func Type of the function object to invoke.
* @param func A valid function object.
*/
@@ -410,13 +409,19 @@ public:
* It is no longer guaranteed that the performance is the best possible, but
* there will be greater control over the order of iteration.
*
* @note
* Empty types aren't explicitly instantiated. Therefore, temporary objects
* are returned during iterations. They can be caught only by copy or with
* const references.
*
* @tparam Comp Type of component to use to enforce the iteration order.
* @tparam Func Type of the function object to invoke.
* @param func A valid function object.
*/
template<typename Comp, typename Func>
inline void each(Func func) const {
each<Comp>(type_list_cat_t<std::conditional_t<std::is_same_v<Comp, Component>, type_list<>, type_list<Component>>...>{}, std::move(func));
using other_type = type_list_cat_t<std::conditional_t<std::is_same_v<Comp, Component>, type_list<>, type_list<Component>>...>;
each<Comp>(other_type{}, std::move(func));
}
private:
@@ -470,7 +475,7 @@ class basic_view<Entity, Component> {
public:
/*! @brief Type of component iterated by the view. */
using raw_type = std::remove_reference_t<decltype(std::declval<pool_type>().get(0))>;
using raw_type = Component;
/*! @brief Underlying entity identifier. */
using entity_type = typename pool_type::entity_type;
/*! @brief Unsigned integer type. */
@@ -504,11 +509,6 @@ public:
* There are no guarantees on the order of the components. Use `begin` and
* `end` if you want to iterate the view in the expected order.
*
* @warning
* Empty components aren't explicitly instantiated. Only one instance of the
* given type is created. Therefore, this function always returns a pointer
* to that instance.
*
* @return A pointer to the array of components.
*/
raw_type * raw() const ENTT_NOEXCEPT {
@@ -612,7 +612,7 @@ public:
* @param entt A valid entity identifier.
* @return The component assigned to the entity.
*/
raw_type & get(const entity_type entt) const ENTT_NOEXCEPT {
decltype(auto) get(const entity_type entt) const ENTT_NOEXCEPT {
ENTT_ASSERT(contains(entt));
return pool->get(entt);
}
@@ -632,12 +632,17 @@ public:
* void(Component &);
* @endcode
*
* @note
* Empty types aren't explicitly instantiated. Therefore, temporary objects
* are returned during iterations. They can be caught only by copy or with
* const references.
*
* @tparam Func Type of the function object to invoke.
* @param func A valid function object.
*/
template<typename Func>
void each(Func func) const {
if constexpr(std::is_invocable_v<Func, std::add_lvalue_reference_t<Component>>) {
if constexpr(std::is_invocable_v<Func, decltype(get({}))>) {
std::for_each(pool->begin(), pool->end(), std::move(func));
} else {
std::for_each(pool->sparse_set<Entity>::begin(), pool->sparse_set<Entity>::end(), [&func, raw = pool->begin()](const auto entt) mutable {

View File

@@ -242,10 +242,8 @@ TEST(NonOwningGroup, ConstNonConstAndAllInBetween) {
ASSERT_EQ(group.size(), decltype(group.size()){1});
ASSERT_TRUE((std::is_same_v<decltype(group.get<int>(0)), int &>));
ASSERT_TRUE((std::is_same_v<decltype(group.get<const int>(0)), const int &>));
ASSERT_TRUE((std::is_same_v<decltype(group.get<const char>(0)), const char &>));
ASSERT_TRUE((std::is_same_v<decltype(group.get<int, const char>(0)), std::tuple<int &, const char &>>));
ASSERT_TRUE((std::is_same_v<decltype(group.get<const int, const char>(0)), std::tuple<const int &, const char &>>));
ASSERT_TRUE((std::is_same_v<decltype(group.raw<const char>()), const char *>));
ASSERT_TRUE((std::is_same_v<decltype(group.raw<int>()), int *>));
@@ -740,13 +738,10 @@ TEST(OwningGroup, ConstNonConstAndAllInBetween) {
ASSERT_EQ(group.size(), decltype(group.size()){1});
ASSERT_TRUE((std::is_same_v<decltype(group.get<int>(0)), int &>));
ASSERT_TRUE((std::is_same_v<decltype(group.get<const int>(0)), const int &>));
ASSERT_TRUE((std::is_same_v<decltype(group.get<const char>(0)), const char &>));
ASSERT_TRUE((std::is_same_v<decltype(group.get<double>(0)), double &>));
ASSERT_TRUE((std::is_same_v<decltype(group.get<const double>(0)), const double &>));
ASSERT_TRUE((std::is_same_v<decltype(group.get<const float>(0)), const float &>));
ASSERT_TRUE((std::is_same_v<decltype(group.get<int, const char, double, const float>(0)), std::tuple<int &, const char &, double &, const float &>>));
ASSERT_TRUE((std::is_same_v<decltype(group.get<const int, const char, const double, const float>(0)), std::tuple<const int &, const char &, const double &, const float &>>));
ASSERT_TRUE((std::is_same_v<decltype(group.raw<const float>()), const float *>));
ASSERT_TRUE((std::is_same_v<decltype(group.raw<double>()), double *>));
ASSERT_TRUE((std::is_same_v<decltype(group.raw<const char>()), const char *>));

View File

@@ -1002,7 +1002,7 @@ TEST(Registry, CreateManyEntitiesAtOnce) {
TEST(Registry, CreateAnEntityWithComponents) {
entt::registry registry;
const auto &[entity, ivalue, cvalue] = registry.create<int, char>();
auto &&[entity, ivalue, cvalue] = registry.create<int, char>();
ASSERT_FALSE(registry.empty<int>());
ASSERT_FALSE(registry.empty<char>());

View File

@@ -87,8 +87,6 @@ TEST(Storage, EmptyType) {
ASSERT_TRUE(set.has(42));
ASSERT_TRUE(set.has(99));
ASSERT_EQ(set.try_get(42), nullptr);
ASSERT_EQ(std::as_const(set).try_get(42), std::as_const(set).try_get(99));
auto &&component = set.get(42);
@@ -314,15 +312,6 @@ TEST(Storage, Raw) {
ASSERT_EQ(*(set.raw() + 2u), 9);
}
TEST(Storage, RawEmptyType) {
entt::storage<std::uint64_t, empty_type> set;
set.construct(3);
ASSERT_EQ(set.raw(), std::as_const(set).raw());
ASSERT_EQ(set.try_get(3), set.raw());
}
TEST(Storage, SortOrdered) {
entt::storage<std::uint64_t, boxed_int> set;

View File

@@ -398,10 +398,8 @@ TEST(MultipleComponentView, ConstNonConstAndAllInBetween) {
ASSERT_EQ(view.size(), decltype(view.size()){1});
ASSERT_TRUE((std::is_same_v<decltype(view.get<int>(0)), int &>));
ASSERT_TRUE((std::is_same_v<decltype(view.get<const int>(0)), const int &>));
ASSERT_TRUE((std::is_same_v<decltype(view.get<const char>(0)), const char &>));
ASSERT_TRUE((std::is_same_v<decltype(view.get<int, const char>(0)), std::tuple<int &, const char &>>));
ASSERT_TRUE((std::is_same_v<decltype(view.get<const int, const char>(0)), std::tuple<const int &, const char &>>));
ASSERT_TRUE((std::is_same_v<decltype(view.raw<const char>()), const char *>));
ASSERT_TRUE((std::is_same_v<decltype(view.raw<int>()), int *>));