Przeglądaj źródła

sparse_set/view:
* added basic_sparse_set<E>::current to return known version of identifiers in set
* sparse arrays also contain updated entity versions, no more unused bits
* basic_sparse_set<E>::contains performs a strict version check
* sparse sets manage correctly foreign entities in all cases now
* no tombstone checks for multi-type views, zero-cost pointer stability model

Michele Caini 4 lat temu
rodzic
commit
3c4d13d8b9
5 zmienionych plików z 409 dodań i 288 usunięć
  1. 5 4
      TODO
  2. 73 142
      docs/md/entity.md
  3. 68 44
      src/entt/entity/sparse_set.hpp
  4. 18 30
      src/entt/entity/view.hpp
  5. 245 68
      test/entt/entity/sparse_set.cpp

+ 5 - 4
TODO

@@ -5,19 +5,20 @@
 * custom pools example (multi instance, tables, enable/disable, and so on...)
 
 WIP:
+* fast-contains for sparse sets (low prio but nice-to-have)
 * runtime components (registry), runtime events (dispatcher/emitter), ...
-* single prop call with operator() for meta factories (to cut down instantiations)
-* remove the tombstone check (version check + null version)
+* runtime_view/registry, remove reference to basic_sparse_set<E>
+* make pools available (registry/view/group), review operator| for views, make views accept registry to ctor
+* make view.lead() or similar available to return leading pool (useful for mt)
 * dedicated entity storage, in-place O(1) release/destroy
+* deprecate registry::entity, registry::version (use entt_traits)
 * general purpose paged vector container
 * custom allocators all over
 
 WIP:
-* payload tracking destroy mixin for pools
 * make sparse_set/storage adhere to AllocatorAwareContainer requirements
 * make it possible to register externally managed pools with the registry (allow for system centric mode)
 * registry: switch to the udata/mixin model and get rid of poly storage, use pointer to sparse set only for pools, discard pool_data type.
-* make pools available (registry/view/group), review operator| for views
 * resource, forward the id to the loader from the cache and if constexpr the call to load, update doc and describe customization points
 * make it possible to create views of the type `view<T, T>`, add get by index and such, allow to register custom pools by name with the registry
 * add user data to type_info

+ 73 - 142
docs/md/entity.md

@@ -27,8 +27,8 @@
     * [Organizer](#organizer)
   * [Context variables](#context-variables)
     * [Aliased properties](#aliased-properties)
-  * [In-place delete](#in-place-delete)
-    * [Pointer stability](#pointer-stability)
+  * [Pointer stability](#pointer-stability)
+    * [In-place delete](#in-place-delete)
     * [Hierarchies and the like](#hierarchies-and-the-like)
   * [Making the most of range-destroy](#making-the-most-of-range-destroy)
   * [Meet the runtime](#meet-the-runtime)
@@ -48,8 +48,6 @@
     * [Nested groups](#nested-groups)
   * [Types: const, non-const and all in between](#types-const-non-const-and-all-in-between)
   * [Give me everything](#give-me-everything)
-  * [Stable storage](#stable-storage)
-    * [No policy is the best policy](#no-policy-is-the-best-policy)
   * [What is allowed and what is not](#what-is-allowed-and-what-is-not)
     * [More performance, more constraints](#more-performance-more-constraints)
 * [Empty type optimization](#empty-type-optimization)
@@ -981,7 +979,24 @@ const my_type &var = registry.ctx<const my_type>();
 Aliased properties can be unset and are overwritten when `set` is invoked, as it
 happens with standard variables.
 
-## In-place delete
+## Pointer stability
+
+The ability to achieve pointer stability for one, several or all components is a
+direct consequence of the design of `EnTT` and of its default storage.<br/>
+In fact, although it contains what is commonly referred to as a _packed array_,
+the default storage is paged and doesn't suffer from invalidation of references
+when it runs out of space and has to reallocate.<br/>
+However, this isn't enough to ensure pointer stability in case of deletion. For
+this reason, a _stable_ deletion method is also offered. This one is such that
+the position of the elements is preserved by creating tombstones upon deletion
+rather than trying to fill the holes that are created.
+
+For performance reasons, `EnTT` will also favor storage compaction in all cases,
+although often accessing a component occurs mostly randomly or traversing pools
+in a non-linear order on the user side (as in the case of a hierarchy).<br/>
+In other words, pointer stability is not automatic but is enabled on request.
+
+### In-place delete
 
 By default, `EnTT` keeps all pools compact when a component is removed. This is
 done through a swap-and-pop between the removed item and the one occupying the
@@ -1008,7 +1023,7 @@ struct basic_component_traits {
 Where `in_place_delete` instructs the library on the deletion policy for a given
 type while `ignore_if_empty` selectively disables empty type optimization.<br/>
 The `component_traits` class template is _sfinae-friendly_, it supports single-
-and multi-type specializations as well as feature-based ones:
+and multi type specializations as well as feature-based ones:
 
 ```cpp
 template<>
@@ -1019,38 +1034,25 @@ struct entt::component_traits<position>: basic_component_traits {
 
 This will ensure in-place deletion for the `position` component without further
 user intervention.<br/>
-Pools, views and groups will adapt accordingly when they detect a storage with a
-different deletion policy than the default. No specific action is required from
-the user once in-place deletion is enabled.
-
-### Pointer stability
+Views and groups adapt accordingly when they detect a storage with a different
+deletion policy than the default. No specific action is required from the user
+once in-place deletion is enabled. In particular:
 
-The ability to achieve pointer stability for one, several or all components is a
-direct consequence of the design of `EnTT` and of its default storage.<br/>
-In fact, although it contains what is commonly referred to as a _packed array_,
-the default storage is paged and doesn't suffer from invalidation of references
-when it runs out of space and has to reallocate.<br/>
-However, this isn't enough to ensure pointer stability in case of deletion. For
-this reason, a _stable_ deletion method is also offered. This one is such that
-the position of the elements is preserved by creating tombstones upon deletion
-rather than trying to fill the holes that are created.
+* Groups are incompatible with stable storage and will trigger a compile-time
+  error if detected.
 
-For performance reasons, `EnTT` will also favor storage compaction in all cases,
-although often accessing a component occurs mostly randomly or traversing pools
-in a non-linear order on the user side (as in the case of a hierarchy).<br/>
-In other words, pointer stability is not automatic but is enabled on request. To
-have it at the project level and for all components, it's required to partially
-specialize the `component_traits` class for all possible types:
+* Multi type views are completely transparent to storage policies.
 
-```cpp
-template<typename Type>
-struct entt::component_traits<Type>: basic_component_traits {
-    using in_place_delete = std::true_type;
-};
-```
+* Single type views for stable storage types offer the same interface of multi
+  type views. For example, only `size_hint` is available and it's not possible
+  to directly access the raw representation of entities and components.
 
-Because of how C++ works, this specialization will obviously have to be visible
-every time operations are performed on a storage.
+In other words, the more generic version of a view will be provided in case of
+stable storage, even for single components, always supported by an appropriate
+iteration policy if required.<br/>
+The latter will be such that in no case will a tombstone be returned from the
+view itself, regardless of the iteration method. Similarly, no non-existent
+components will be accessed, which could result in an UB otherwise.
 
 ### Hierarchies and the like
 
@@ -1464,24 +1466,24 @@ A view behaves differently if it's constructed for a single component or if it
 has been created to iterate multiple components. Even the API is slightly
 different in the two cases.
 
-Single component views are specialized in order to give a boost in terms of
-performance in all the situations. This kind of views can access the underlying
-data structures directly and avoid superfluous checks. There is nothing as fast
-as a single component view. In fact, they walk through a packed (actually paged)
-array of components and return them one at a time.<br/>
-Single component views also offer a bunch of functionalities to get the number
-of entities they are going to return and a raw access to the entity list as well
-as to the component list. It's also possible to ask a view if it contains a
-given entity.<br/>
+Single type views are specialized to give a boost in terms of performance in all
+the situations. This kind of views can access the underlying data structures
+directly and avoid superfluous checks. There is nothing as fast as a single type
+view. In fact, they walk through a packed (actually paged) array of components
+and return them one at a time.<br/>
+Single type views also offer a bunch of functionalities to get the number of
+entities they are going to return and a raw access to the entity list as well as
+to the component list. It's also possible to ask a view if it contains a given
+entity.<br/>
 Refer to the inline documentation for all the details.
 
-Multi component views iterate entities that have at least all the given
-components in their bags. During construction, these views look at the number of
-entities available for each component and pick up a reference to the smallest
-set of candidates in order to speed up iterations.<br/>
-They offer fewer functionalities than single component views. In particular,
-a multi component view exposes utility functions to get the estimated number of
-entities it is going to return and to know if it contains a given entity.<br/>
+Multi type views iterate entities that have at least all the given components in
+their bags. During construction, these views look at the number of entities
+available for each component and pick up a reference to the smallest set of
+candidates in order to speed up iterations.<br/>
+They offer fewer functionalities than single type views. In particular, a multi
+type view exposes utility functions to get the estimated number of entities it
+is going to return and to know if it contains a given entity.<br/>
 Refer to the inline documentation for all the details.
 
 There is no need to store views aside for they are extremely cheap to construct,
@@ -1492,10 +1494,10 @@ Views also return newly created and correctly initialized iterators whenever
 Views share the way they are created by means of a registry:
 
 ```cpp
-// single component view
+// single type view
 auto single = registry.view<position>();
 
-// multi component view
+// multi type view
 auto multi = registry.view<position, velocity>();
 ```
 
@@ -1546,9 +1548,9 @@ iterations.<br/>
 Since they aren't explicitly instantiated, empty components aren't returned in
 any case.
 
-As a side note, in the case of single component views, `get` accepts but doesn't
+As a side note, in the case of single type views, `get` accepts but doesn't
 strictly require a template parameter, since the type is implicitly defined.
-However, when the type isn't specified, for consistency with the multi component
+However, when the type isn't specified, for consistency with the multi type
 view, the instance will be returned using a tuple:
 
 ```cpp
@@ -1590,12 +1592,12 @@ Runtime views iterate entities that have at least all the given components in
 their bags. During construction, these views look at the number of entities
 available for each component and pick up a reference to the smallest set of
 candidates in order to speed up iterations.<br/>
-They offer more or less the same functionalities of a multi component view.
-However, they don't expose a `get` member function and users should refer to the
-registry that generated the view to access components. In particular, a runtime
-view exposes utility functions to get the estimated number of entities it is
-going to return and to know whether it's empty or not. It's also possible to ask
-a runtime view if it contains a given entity.<br/>
+They offer more or less the same functionalities of a multi type view. However,
+they don't expose a `get` member function and users should refer to the registry
+that generated the view to access components. In particular, a runtime view
+exposes utility functions to get the estimated number of entities it is going to
+return and to know whether it's empty or not. It's also possible to ask a
+runtime view if it contains a given entity.<br/>
 Refer to the inline documentation for all the details.
 
 Runtime views are pretty cheap to construct and should not be stored aside in
@@ -1638,7 +1640,7 @@ use runtime views as their performance are inferior to those of the other views.
 ## Groups
 
 Groups are meant to iterate multiple components at once and to offer a faster
-alternative to multi component views.<br/>
+alternative to multi type views.<br/>
 Groups overcome the performance of the other tools available but require to get
 the ownership of components and this sets some constraints on pools. On the
 other side, groups aren't an automatism that increases memory consumption,
@@ -1977,77 +1979,6 @@ In general, all these functions can result in poor performance.<br/>
 entity. For similar reasons, `orphans` can be even slower. Both functions should
 not be used frequently to avoid the risk of a performance hit.
 
-## Stable storage
-
-Since it's possible to have completely stable storage in `EnTT`, it's also
-required that all views and groups behave accordingly.<br/>
-In general, this aspect is quite transparent to the user who doesn't have to do
-anything in the vast majority of cases. In particular:
-
-* Groups are incompatible with stable storage and will trigger a compile-time
-  error if detected.
-
-* Views detect the type of storage with the most stringent requirements when
-  built and self-configure themselves to use the correct iteration policy.
-
-* Views created as view packs adjust their policy by choosing the most stringent
-  among those available.
-
-The policy adopted doesn't emerge from the view type, although it's available
-through the `storage_policy` alias.<br/>
-However, this can affect the feature set offered by the view itself. In the case
-of storage that also support tombstones, all views (even single-component ones)
-will always behave as a multi-type views. Therefore, for example, it won't be
-possible to directly access the raw representation of entities and components.
-
-In other words, the more generic version of a view will be provided in case of
-stable storage, even for single components, always supported by an appropriate
-iteration policy.<br/>
-The latter will be such that in no case will a tombstone be returned from the
-view itself, regardless of the iteration method. Similarly, no non-existent
-components will be accessed, which could result in an UB otherwise.
-
-### No policy is the best policy
-
-When a view is iterated and the type leading the iteration undergoes in-place
-deletion, a dedicated policy is used to skip any tombstones.<br/>
-As negligible as it may be (and the advice is to always measure before trying to
-optimize things that don't need it), this adds a cost to the iteration itself.
-
-As for single type iterations, this check cannot be avoided. On the other hand,
-if a type is accessed mainly through linear iterations, the error is probably in
-the use of stable storage that doesn't allow to rearrange the elements in order
-to keep them tightly packed.<br/>
-On the other hand, in many cases it's possible to force the use of no policy for
-multi-type iterations. By default, a view uses the most stringent policy unless
-instructed otherwise, primarily because it does an opaque iteration over deleted
-types. However, by specifying the type to lead the iteration, it has enough
-information to choose the most suitable policy (hopefully, no policy at all).
-
-For example, if the `position` type has stable storage and the `velocity` type
-does not, the following iteration will use a policy to skip all tombstones:
-
-```cpp
-for(auto [entity, pos, vel]: registry.view<position, velocity>().each()) {
-    /* ... */
-}
-```
-
-However, by requiring `velocity` to lead the iteration, the view will be able to
-choose the most suitable policy, that is no policy at all in this case:
-
-```cpp
-for(auto [entity, pos, vel]: registry.view<position, velocity>().each<velocity>()) {
-    /* ... */
-}
-```
-
-Forcing the use of no policy means returning to the iteration logic normally
-adopted for views that don't contain types with stable storage, that is the
-iteration process that has accompanied `EnTT` since the beginning.<br/>
-The same technique can be used with all overloads of the `each` function as well
-as with the `begin`/`end` template functions of a view.
-
 ## What is allowed and what is not
 
 Most of the _ECS_ available out there don't allow to create and destroy entities
@@ -2122,24 +2053,24 @@ limitations to the destruction of components and entities.<br/>
 Fortunately, this isn't always true. In fact, it almost never is and this
 happens only under certain conditions. In particular:
 
-* Iterating a type of component that is part of a group with a single component
-  view and adding to an entity all the components required to get it into the
-  group may invalidate the iterators.
+* Iterating a type of component that is part of a group with a single type view
+  and adding to an entity all the components required to get it into the group
+  may invalidate the iterators.
 
-* Iterating a type of component that is part of a group with a multi component
-  view and adding to an entity all the components required to get it into the
-  group can invalidate the iterators, unless users specify another type of
-  component to use to induce the order of iteration of the view (in this case,
-  the former is treated as a free type and isn't affected by the limitation).
+* Iterating a type of component that is part of a group with a multi type view
+  and adding to an entity all the components required to get it into the group
+  can invalidate the iterators, unless users specify another type of component
+  to use to induce the order of iteration of the view (in this case, the former
+  is treated as a free type and isn't affected by the limitation).
 
 In other words, the limitation doesn't exist as long as a type is treated as a
-free type (as an example with multi component views and partial- or non-owning
+free type (as an example with multi type views and partial- or non-owning
 groups) or iterated with its own group, but it can occur if the type is used as
 a main type to rule on an iteration.<br/>
 This happens because groups own the pools of their components and organize the
 data internally to maximize performance. Because of that, full consistency for
 owned components is guaranteed only when they are iterated as part of their
-groups or as free types with multi component views and groups in general.
+groups or as free types with multi type views and groups in general.
 
 # Empty type optimization
 

+ 68 - 44
src/entt/entity/sparse_set.hpp

@@ -52,8 +52,8 @@ enum class deletion_policy: std::uint8_t {
  */
 template<typename Entity, typename Allocator>
 class basic_sparse_set {
-    static constexpr auto growth_factor = 1.5;
-    static constexpr auto sparse_page = ENTT_SPARSE_PAGE;
+    static constexpr auto growth_factor_v = 1.5;
+    static constexpr auto sparse_page_v = ENTT_SPARSE_PAGE;
 
     using allocator_traits = std::allocator_traits<Allocator>;
 
@@ -168,12 +168,12 @@ class basic_sparse_set {
         difference_type index;
     };
 
-    [[nodiscard]] static auto page(const Entity entt) ENTT_NOEXCEPT {
-        return static_cast<size_type>(entity_traits::to_entity(entt) / sparse_page);
+    [[nodiscard]] static constexpr auto page(const Entity entt) ENTT_NOEXCEPT {
+        return static_cast<size_type>(entity_traits::to_entity(entt) / sparse_page_v);
     }
 
-    [[nodiscard]] static auto offset(const Entity entt) ENTT_NOEXCEPT {
-        return static_cast<size_type>(entity_traits::to_entity(entt) & (sparse_page - 1));
+    [[nodiscard]] static constexpr auto offset(const Entity entt) ENTT_NOEXCEPT {
+        return static_cast<size_type>(entity_traits::to_entity(entt) & (sparse_page_v - 1));
     }
 
     [[nodiscard]] auto assure_page(const std::size_t idx) {
@@ -195,8 +195,8 @@ class basic_sparse_set {
         }
 
         if(!sparse[idx]) {
-            sparse[idx] = alloc_traits::allocate(reserved.first(), sparse_page);
-            std::uninitialized_fill(sparse[idx], sparse[idx] + sparse_page, null);
+            sparse[idx] = alloc_traits::allocate(reserved.first(), sparse_page_v);
+            std::uninitialized_fill(sparse[idx], sparse[idx] + sparse_page_v, null);
         }
 
         return sparse[idx];
@@ -232,8 +232,8 @@ class basic_sparse_set {
 
             for(size_type pos{}; pos < bucket; ++pos) {
                 if(sparse[pos]) {
-                    std::destroy(sparse[pos], sparse[pos] + sparse_page);
-                    alloc_traits::deallocate(allocator, sparse[pos], sparse_page);
+                    std::destroy(sparse[pos], sparse[pos] + sparse_page_v);
+                    alloc_traits::deallocate(allocator, sparse[pos], sparse_page_v);
                 }
 
                 alloc_ptr_traits::destroy(allocator_ptr, std::addressof(sparse[pos]));
@@ -252,16 +252,17 @@ protected:
 
     /**
      * @brief Attempts to erase an entity from the internal packed array.
-     * @param entt A valid entity identifier.
+     * @param entt A valid identifier.
      */
     virtual void swap_and_pop(const Entity entt, void *) {
         auto &ref = sparse[page(entt)][offset(entt)];
         const auto pos = static_cast<size_type>(entity_traits::to_entity(ref));
-        ENTT_ASSERT(packed[pos] == entt, "Invalid entity identifier");
+        ENTT_ASSERT(packed[pos] == entt, "Invalid identifier");
         auto &last = packed[--count];
 
         packed[pos] = last;
-        sparse[page(last)][offset(last)] = ref;
+        auto &elem = sparse[page(last)][offset(last)];
+        elem = entity_traits::combine(entity_traits::to_integral(ref), entity_traits::to_integral(elem));
         // lazy self-assignment guard
         ref = null;
         // unnecessary but it helps to detect nasty bugs
@@ -270,12 +271,12 @@ protected:
 
     /**
      * @brief Attempts to erase an entity from the internal packed array.
-     * @param entt A valid entity identifier.
+     * @param entt A valid identifier.
      */
     virtual void in_place_pop(const Entity entt, void *) {
         auto &ref = sparse[page(entt)][offset(entt)];
         const auto pos = static_cast<size_type>(entity_traits::to_entity(ref));
-        ENTT_ASSERT(packed[pos] == entt, "Invalid entity identifier");
+        ENTT_ASSERT(packed[pos] == entt, "Invalid identifier");
 
         packed[pos] = std::exchange(free_list, entity_traits::combine(static_cast<typename entity_traits::entity_type>(pos), entity_traits::reserved));
         // lazy self-assignment guard
@@ -287,6 +288,8 @@ public:
     using allocator_type = Allocator;
     /*! @brief Underlying entity identifier. */
     using entity_type = Entity;
+    /*! @brief Underlying version type. */
+    using version_type = typename entity_traits::version_type;
     /*! @brief Unsigned integer type. */
     using size_type = std::size_t;
     /*! @brief Pointer type to contained entities. */
@@ -427,7 +430,7 @@ public:
      * @return Extent of the sparse set.
      */
     [[nodiscard]] size_type extent() const ENTT_NOEXCEPT {
-        return bucket * sparse_page;
+        return bucket * sparse_page_v;
     }
 
     /**
@@ -517,7 +520,7 @@ public:
 
     /**
      * @brief Finds an entity.
-     * @param entt A valid entity identifier.
+     * @param entt A valid identifier.
      * @return An iterator to the given entity if it's found, past the end
      * iterator otherwise.
      */
@@ -527,14 +530,31 @@ public:
 
     /**
      * @brief Checks if a sparse set contains an entity.
-     * @param entt A valid entity identifier.
+     * @param entt A valid identifier.
      * @return True if the sparse set contains the entity, false otherwise.
      */
     [[nodiscard]] bool contains(const entity_type entt) const ENTT_NOEXCEPT {
-        ENTT_ASSERT(entt != tombstone && entt != null, "Invalid entity");
-        const auto curr = page(entt);
-        // testing versions permits to avoid accessing the packed array
-        return (curr < bucket && sparse[curr] && sparse[curr][offset(entt)] != null);
+        if(const auto curr = page(entt); curr < bucket && sparse[curr]) {
+            constexpr auto cap = entity_traits::to_entity(entt::null);
+            // testing versions permits to avoid accessing the packed array
+            return (((~cap & entity_traits::to_integral(entt)) ^ entity_traits::to_integral(sparse[curr][offset(entt)])) < cap);
+        }
+
+        return false;
+    }
+
+    /**
+     * @brief Returns the contained version for an identifier.
+     * @param entt A valid identifier.
+     * @return The version for the given identifier if present, the tombstone
+     * version otherwise.
+     */
+    [[nodiscard]] version_type current(const entity_type entt) const {
+        if(const auto curr = page(entt); curr < bucket && sparse[curr]) {
+            return entity_traits::to_version(sparse[curr][offset(entt)]);
+        }
+
+        return entity_traits::to_version(tombstone);
     }
 
     /**
@@ -544,7 +564,7 @@ public:
      * Attempting to get the position of an entity that doesn't belong to the
      * sparse set results in undefined behavior.
      *
-     * @param entt A valid entity identifier.
+     * @param entt A valid identifier.
      * @return The position of the entity in the sparse set.
      */
     [[nodiscard]] size_type index(const entity_type entt) const ENTT_NOEXCEPT {
@@ -578,18 +598,19 @@ public:
      * Attempting to assign an entity that already belongs to the sparse set
      * results in undefined behavior.
      *
-     * @param entt A valid entity identifier.
+     * @param entt A valid identifier.
      * @return The slot used for insertion.
      */
     size_type emplace_back(const entity_type entt) {
-        ENTT_ASSERT(!contains(entt), "Set already contains entity");
+        ENTT_ASSERT(current(entt) == entity_traits::to_version(tombstone), "Slot not available");
 
         if(const auto len = reserved.second(); count == len) {
-            const size_type sz = static_cast<size_type>(len * growth_factor);
+            const size_type sz = static_cast<size_type>(len * growth_factor_v);
             resize_packed(sz + !(sz > len));
         }
 
-        assure_page(page(entt))[offset(entt)] = entity_traits::combine(static_cast<typename entity_traits::entity_type>(count), {});
+        const auto entity = static_cast<typename entity_traits::entity_type>(count);
+        assure_page(page(entt))[offset(entt)] = entity_traits::combine(entity, entity_traits::to_integral(entt));
         packed[count] = entt;
         return count++;
     }
@@ -601,16 +622,16 @@ public:
      * Attempting to assign an entity that already belongs to the sparse set
      * results in undefined behavior.
      *
-     * @param entt A valid entity identifier.
+     * @param entt A valid identifier.
      * @return The slot used for insertion.
      */
     size_type emplace(const entity_type entt) {
         if(free_list == null) {
             return emplace_back(entt);
         } else {
-            ENTT_ASSERT(!contains(entt), "Set already contains entity");
+            ENTT_ASSERT(current(entt) == entity_traits::to_version(tombstone), "Slot not available");
+            assure_page(page(entt))[offset(entt)] = entity_traits::combine(entity_traits::to_integral(free_list), entity_traits::to_integral(entt));
             const auto pos = static_cast<size_type>(entity_traits::to_entity(free_list));
-            assure_page(page(entt))[offset(entt)] = entity_traits::combine(static_cast<typename entity_traits::entity_type>(pos), {});
             free_list = std::exchange(packed[pos], entt);
             return pos;
         }
@@ -633,8 +654,9 @@ public:
 
         for(; first != last; ++first) {
             const auto entt = *first;
-            ENTT_ASSERT(!contains(entt), "Set already contains entity");
-            assure_page(page(entt))[offset(entt)] = entity_traits::combine(static_cast<typename entity_traits::entity_type>(count), {});
+            ENTT_ASSERT(current(entt) == entity_traits::to_version(tombstone), "Slot not available");
+            const auto entity = static_cast<typename entity_traits::entity_type>(count);
+            assure_page(page(entt))[offset(entt)] = entity_traits::combine(entity, entity_traits::to_integral(entt));
             packed[count++] = entt;
         }
     }
@@ -646,7 +668,7 @@ public:
      * Attempting to erase an entity that doesn't belong to the sparse set
      * results in undefined behavior.
      *
-     * @param entt A valid entity identifier.
+     * @param entt A valid identifier.
      * @param ud Optional user data that are forwarded as-is to derived classes.
      */
     void erase(const entity_type entt, void *ud = nullptr) {
@@ -673,7 +695,7 @@ public:
 
     /**
      * @brief Removes an entity from a sparse set if it exists.
-     * @param entt A valid entity identifier.
+     * @param entt A valid identifier.
      * @param ud Optional user data that are forwarded as-is to derived classes.
      * @return True if the entity is actually removed, false otherwise.
      */
@@ -710,7 +732,8 @@ public:
                 --next;
                 move_and_pop(next, pos);
                 std::swap(packed[next], packed[pos]);
-                sparse[page(packed[pos])][offset(packed[pos])] = entity_traits::combine(static_cast<const typename entity_traits::entity_type>(pos), {});
+                const auto entity = static_cast<typename entity_traits::entity_type>(pos);
+                sparse[page(packed[pos])][offset(packed[pos])] = entity_traits::combine(entity, entity_traits::to_integral(packed[pos]));
                 *it = entity_traits::combine(static_cast<typename entity_traits::entity_type>(next), entity_traits::reserved);
                 for(; next && packed[next - 1u] == tombstone; --next);
             }
@@ -730,22 +753,22 @@ public:
      * Attempting to swap entities that don't belong to the sparse set results
      * in undefined behavior.
      *
-     * @param lhs A valid entity identifier.
-     * @param rhs A valid entity identifier.
+     * @param lhs A valid identifier.
+     * @param rhs A valid identifier.
      */
     void swap(const entity_type lhs, const entity_type rhs) {
-        ENTT_ASSERT(contains(lhs), "Set does not contain entity");
-        ENTT_ASSERT(contains(rhs), "Set does not contain entity");
+        ENTT_ASSERT(contains(lhs) && contains(rhs), "Set does not contain entities");
 
         auto &entt = sparse[page(lhs)][offset(lhs)];
         auto &other = sparse[page(rhs)][offset(rhs)];
 
-        const auto from = static_cast<size_type>(entity_traits::to_entity(entt));
-        const auto to = static_cast<size_type>(entity_traits::to_entity(other));
+        const auto from = entity_traits::to_entity(entt);
+        const auto to = entity_traits::to_entity(other);
 
         // basic no-leak guarantee (with invalid state) if swapping throws
-        swap_at(from, to);
-        std::swap(entt, other);
+        swap_at(static_cast<size_type>(from), static_cast<size_type>(to));
+        entt = entity_traits::combine(to, entity_traits::to_integral(packed[from]));
+        other = entity_traits::combine(from, entity_traits::to_integral(packed[to]));
         std::swap(packed[from], packed[to]);
     }
 
@@ -796,7 +819,8 @@ public:
                 const auto entt = packed[curr];
 
                 swap_at(next, idx);
-                sparse[page(entt)][offset(entt)] = entity_traits::combine(static_cast<typename entity_traits::entity_type>(curr), {});
+                const auto entity = static_cast<typename entity_traits::entity_type>(curr);
+                sparse[page(entt)][offset(entt)] = entity_traits::combine(entity, entity_traits::to_integral(packed[curr]));
                 curr = std::exchange(next, idx);
             }
         }

+ 18 - 30
src/entt/entity/view.hpp

@@ -110,13 +110,14 @@ private:
 };
 
 
-template<typename Type, typename It, std::size_t Component, std::size_t Exclude, typename Policy>
+template<typename Type, typename It, std::size_t Component, std::size_t Exclude>
 class view_iterator final {
+    static constexpr auto is_multi_type_v = ((Component + Exclude) != 0u);
+
     [[nodiscard]] bool valid() const {
-        const auto entt = *it;
-        return Policy::accept(entt)
-            && std::apply([entt](const auto *... curr) { return (curr->contains(entt) && ...); }, pools)
-            && std::apply([entt](const auto *... curr) { return (!curr->contains(entt) && ...); }, filter);
+        return (is_multi_type_v || (*it != tombstone))
+            && std::apply([entt = *it](const auto *... curr) { return (curr->contains(entt) && ...); }, pools)
+            && std::apply([entt = *it](const auto *... curr) { return (!curr->contains(entt) && ...); }, filter);
     }
 
 public:
@@ -192,19 +193,6 @@ private:
 };
 
 
-template<bool Enable>
-struct policy_dispatcher {
-    template<typename Entity>
-    [[nodiscard]] static constexpr bool accept([[maybe_unused]] const Entity entity) ENTT_NOEXCEPT {
-        if constexpr(Enable) {
-            return entity != tombstone;
-        } else {
-            return true;
-        }
-    }
-};
-
-
 }
 
 
@@ -262,8 +250,8 @@ class basic_view<Entity, get_t<Component...>, exclude_t<Exclude...>> {
     template<typename, typename, typename, typename>
     friend class basic_view;
 
+    static constexpr auto is_multi_type_v = ((sizeof...(Component) + sizeof...(Exclude)) != 1u);
     using basic_common_type = std::common_type_t<typename storage_traits<Entity, std::remove_const_t<Component>>::storage_type::base_type...>;
-    using policy_type = internal::policy_dispatcher<(in_place_delete_v<std::remove_const_t<Component>> || ...)>;
 
     class iterable final {
         template<typename It>
@@ -306,8 +294,8 @@ class basic_view<Entity, get_t<Component...>, exclude_t<Exclude...>> {
         };
 
     public:
-        using iterator = iterable_iterator<internal::view_iterator<basic_common_type, typename basic_common_type::iterator, sizeof...(Component) - 1u, sizeof...(Exclude), policy_type>>;
-        using reverse_iterator = iterable_iterator<internal::view_iterator<basic_common_type, typename basic_common_type::reverse_iterator, sizeof...(Component) - 1u, sizeof...(Exclude), policy_type>>;
+        using iterator = iterable_iterator<internal::view_iterator<basic_common_type, typename basic_common_type::iterator, sizeof...(Component) - 1u, sizeof...(Exclude)>>;
+        using reverse_iterator = iterable_iterator<internal::view_iterator<basic_common_type, typename basic_common_type::reverse_iterator, sizeof...(Component) - 1u, sizeof...(Exclude)>>;
 
         iterable(const basic_view &parent)
             : view{parent}
@@ -361,9 +349,9 @@ public:
     /*! @brief Unsigned integer type. */
     using size_type = std::size_t;
     /*! @brief Bidirectional iterator type. */
-    using iterator = internal::view_iterator<basic_common_type, typename basic_common_type::iterator, sizeof...(Component) - 1u, sizeof...(Exclude), policy_type>;
+    using iterator = internal::view_iterator<basic_common_type, typename basic_common_type::iterator, sizeof...(Component) - 1u, sizeof...(Exclude)>;
     /*! @brief Reverse iterator type. */
-    using reverse_iterator = internal::view_iterator<basic_common_type, typename basic_common_type::reverse_iterator, sizeof...(Component) - 1u, sizeof...(Exclude), policy_type>;
+    using reverse_iterator = internal::view_iterator<basic_common_type, typename basic_common_type::reverse_iterator, sizeof...(Component) - 1u, sizeof...(Exclude)>;
     /*! @brief Iterable view type. */
     using iterable_view = iterable;
 
@@ -486,7 +474,7 @@ public:
 
     /**
      * @brief Finds an entity.
-     * @param entt A valid entity identifier.
+     * @param entt A valid identifier.
      * @return An iterator to the given entity if it's found, past the end
      * iterator otherwise.
      */
@@ -505,7 +493,7 @@ public:
 
     /**
      * @brief Checks if a view contains an entity.
-     * @param entt A valid entity identifier.
+     * @param entt A valid identifier.
      * @return True if the view contains the given entity, false otherwise.
      */
     [[nodiscard]] bool contains(const entity_type entt) const {
@@ -525,7 +513,7 @@ public:
      * results in undefined behavior.
      *
      * @tparam Comp Types of components to get.
-     * @param entt A valid entity identifier.
+     * @param entt A valid identifier.
      * @return The components assigned to the entity.
      */
     template<typename... Comp>
@@ -585,7 +573,7 @@ public:
     template<typename Comp, typename Func>
     void each(Func func) const {
         for(const auto curr: internal::iterable_storage<Entity, Comp>{*std::get<storage_type<Comp> *>(pools)}) {
-            if(internal::policy_dispatcher<in_place_delete_v<std::remove_const_t<Comp>>>::accept(std::get<0>(curr))
+            if((is_multi_type_v || (std::get<0>(curr) != tombstone))
                 && ((std::is_same_v<Comp, Component> || std::get<storage_type<Component> *>(pools)->contains(std::get<0>(curr))) && ...)
                 && std::apply([entt = std::get<0>(curr)](const auto *... cpool) { return (!cpool->contains(entt) && ...); }, filter))
             {
@@ -840,7 +828,7 @@ public:
 
     /**
      * @brief Finds an entity.
-     * @param entt A valid entity identifier.
+     * @param entt A valid identifier.
      * @return An iterator to the given entity if it's found, past the end
      * iterator otherwise.
      */
@@ -868,7 +856,7 @@ public:
 
     /**
      * @brief Checks if a view contains an entity.
-     * @param entt A valid entity identifier.
+     * @param entt A valid identifier.
      * @return True if the view contains the given entity, false otherwise.
      */
     [[nodiscard]] bool contains(const entity_type entt) const {
@@ -887,7 +875,7 @@ public:
      * results in undefined behavior.
      *
      * @tparam Comp Types of components to get.
-     * @param entt A valid entity identifier.
+     * @param entt A valid identifier.
      * @return The component assigned to the entity.
      */
     template<typename... Comp>

+ 245 - 68
test/entt/entity/sparse_set.cpp

@@ -76,20 +76,29 @@ TEST(SparseSet, Functionalities) {
 }
 
 TEST(SparseSet, Contains) {
+    using traits_type = entt::entt_traits<entt::entity>;
+
     entt::sparse_set set{entt::deletion_policy::in_place};
 
     set.emplace(entt::entity{0});
     set.emplace(entt::entity{3});
     set.emplace(entt::entity{42});
     set.emplace(entt::entity{99});
+    set.emplace(traits_type::construct(1, 5));
 
-    set.emplace(entt::entity{1});
+    ASSERT_FALSE(set.contains(entt::null));
+    ASSERT_FALSE(set.contains(entt::tombstone));
 
     ASSERT_TRUE(set.contains(entt::entity{0}));
     ASSERT_TRUE(set.contains(entt::entity{3}));
     ASSERT_TRUE(set.contains(entt::entity{42}));
     ASSERT_TRUE(set.contains(entt::entity{99}));
-    ASSERT_TRUE(set.contains(entt::entity{1}));
+    ASSERT_FALSE(set.contains(entt::entity{1}));
+    ASSERT_TRUE(set.contains(traits_type::construct(1, 5)));
+
+    ASSERT_TRUE(set.contains(traits_type::construct(3, 0)));
+    ASSERT_FALSE(set.contains(traits_type::construct(42, 1)));
+    ASSERT_FALSE(set.contains(traits_type::construct(99, traits_type::to_version(entt::tombstone))));
 
     set.erase(entt::entity{0});
     set.erase(entt::entity{3});
@@ -97,14 +106,54 @@ TEST(SparseSet, Contains) {
     set.remove(entt::entity{42});
     set.remove(entt::entity{99});
 
+    ASSERT_FALSE(set.contains(entt::null));
+    ASSERT_FALSE(set.contains(entt::tombstone));
+
     ASSERT_FALSE(set.contains(entt::entity{0}));
     ASSERT_FALSE(set.contains(entt::entity{3}));
     ASSERT_FALSE(set.contains(entt::entity{42}));
     ASSERT_FALSE(set.contains(entt::entity{99}));
-    ASSERT_TRUE(set.contains(entt::entity{1}));
+    ASSERT_FALSE(set.contains(entt::entity{1}));
+    ASSERT_TRUE(set.contains(traits_type::construct(1, 5)));
 
-    ASSERT_DEATH(static_cast<void>(set.contains(entt::null)), "");
-    ASSERT_DEATH(static_cast<void>(set.contains(entt::tombstone)), "");
+    ASSERT_FALSE(set.contains(traits_type::construct(99, traits_type::to_version(entt::tombstone))));
+}
+
+TEST(SparseSet, Current) {
+    using traits_type = entt::entt_traits<entt::entity>;
+
+    entt::sparse_set set{entt::deletion_policy::in_place};
+
+    set.emplace(traits_type::construct(0, 0));
+    set.emplace(traits_type::construct(3, 3));
+
+    ASSERT_NE(set.current(traits_type::construct(0, 0)), traits_type::to_version(entt::tombstone));
+    ASSERT_NE(set.current(traits_type::construct(3, 3)), traits_type::to_version(entt::tombstone));
+    ASSERT_EQ(set.current(traits_type::construct(3, 0)), traits_type::to_version(traits_type::construct(3, 3)));
+    ASSERT_EQ(set.current(traits_type::construct(42, 1)), traits_type::to_version(entt::tombstone));
+
+    set.remove(entt::entity{0});
+
+    ASSERT_EQ(set.current(traits_type::construct(0, 0)), traits_type::to_version(entt::tombstone));
+    ASSERT_EQ(set.current(traits_type::construct(3, 0)), traits_type::to_version(traits_type::construct(3, 3)));
+}
+
+TEST(SparseSet, Index) {
+    using traits_type = entt::entt_traits<entt::entity>;
+
+    entt::sparse_set set{};
+
+    set.emplace(traits_type::construct(0, 0));
+    set.emplace(traits_type::construct(3, 3));
+
+    ASSERT_EQ(set.index(traits_type::construct(0, 0)), 0u);
+    ASSERT_EQ(set.index(traits_type::construct(3, 3)), 1u);
+
+    set.erase(traits_type::construct(0, 0));
+
+    ASSERT_EQ(set.index(traits_type::construct(3, 3)), 0u);
+    ASSERT_DEATH(static_cast<void>(set.index(traits_type::construct(3, 0))), "");
+    ASSERT_DEATH(static_cast<void>(set.index(entt::null)), "");
 }
 
 TEST(SparseSet, Move) {
@@ -174,11 +223,10 @@ TEST(SparseSet, Pagination) {
 }
 
 TEST(SparseSet, Emplace) {
-    entt::sparse_set set{entt::deletion_policy::in_place};
-    entt::entity entities[2u];
+    using traits_type = entt::entt_traits<entt::entity>;
 
-    entities[0u] = entt::entity{3};
-    entities[1u] = entt::entity{42};
+    entt::sparse_set set{entt::deletion_policy::in_place};
+    entt::entity entities[2u]{entt::entity{3}, entt::entity{42}};
 
     ASSERT_TRUE(set.empty());
 
@@ -188,9 +236,9 @@ TEST(SparseSet, Emplace) {
     set.emplace_back(entities[0u]);
     set.emplace(entities[1u]);
 
+    ASSERT_DEATH(set.emplace(traits_type::combine(3, 1)), "");
     ASSERT_DEATH(set.emplace_back(entities[1u]), "");
-    ASSERT_DEATH(set.emplace(entities[0u]), "");
-
+    
     ASSERT_EQ(set.at(0u), entities[1u]);
     ASSERT_EQ(set.at(1u), entities[0u]);
     ASSERT_EQ(set.index(entities[0u]), 1u);
@@ -222,10 +270,7 @@ TEST(SparseSet, EmplaceOutOfBounds) {
 
 TEST(SparseSet, Insert) {
     entt::sparse_set set{entt::deletion_policy::in_place};
-    entt::entity entities[2u];
-
-    entities[0u] = entt::entity{3};
-    entities[1u] = entt::entity{42};
+    entt::entity entities[2u]{entt::entity{3}, entt::entity{42}};
 
     set.emplace(entt::entity{12});
     set.insert(std::end(entities), std::end(entities));
@@ -263,12 +308,10 @@ TEST(SparseSet, Insert) {
 }
 
 TEST(SparseSet, Erase) {
-    entt::sparse_set set;
-    entt::entity entities[3u];
+    using traits_type = entt::entt_traits<entt::entity>;
 
-    entities[0u] = entt::entity{3};
-    entities[1u] = entt::entity{42};
-    entities[2u] = entt::entity{9};
+    entt::sparse_set set;
+    entt::entity entities[3u]{entt::entity{3}, entt::entity{42}, traits_type::construct(9, 3)};
 
     ASSERT_EQ(set.policy(), entt::deletion_policy::swap_and_pop);
     ASSERT_TRUE(set.empty());
@@ -282,33 +325,42 @@ TEST(SparseSet, Erase) {
     set.erase(set.begin(), set.end());
 
     ASSERT_TRUE(set.empty());
+    ASSERT_EQ(set.current(entities[0u]), traits_type::to_version(entt::tombstone));
+    ASSERT_EQ(set.current(entities[1u]), traits_type::to_version(entt::tombstone));
+    ASSERT_EQ(set.current(entities[2u]), traits_type::to_version(entt::tombstone));
 
     set.insert(std::begin(entities), std::end(entities));
     set.erase(entities, entities + 2u);
 
     ASSERT_FALSE(set.empty());
+    ASSERT_EQ(set.current(entities[0u]), traits_type::to_version(entt::tombstone));
+    ASSERT_EQ(set.current(entities[1u]), traits_type::to_version(entt::tombstone));
+    ASSERT_EQ(set.current(entities[2u]), traits_type::to_version(entities[2u]));
     ASSERT_EQ(*set.begin(), entities[2u]);
 
     set.erase(entities[2u]);
 
     ASSERT_DEATH(set.erase(entities[2u]), "");
     ASSERT_TRUE(set.empty());
+    ASSERT_EQ(set.current(entities[2u]), traits_type::to_version(entt::tombstone));
 
     set.insert(std::begin(entities), std::end(entities));
     std::swap(entities[1u], entities[2u]);
     set.erase(entities, entities + 2u);
 
     ASSERT_FALSE(set.empty());
+    ASSERT_EQ(set.current(entities[2u]), traits_type::to_version(entities[2u]));
     ASSERT_EQ(*set.begin(), entities[2u]);
+
+    ASSERT_DEATH(set.erase(traits_type::construct(9, 0)), "");
+    ASSERT_DEATH(set.erase(entt::null), "");
 }
 
 TEST(SparseSet, StableErase) {
-    entt::sparse_set set{entt::deletion_policy::in_place};
-    entt::entity entities[3u];
+    using traits_type = entt::entt_traits<entt::entity>;
 
-    entities[0u] = entt::entity{3};
-    entities[1u] = entt::entity{42};
-    entities[2u] = entt::entity{9};
+    entt::sparse_set set{entt::deletion_policy::in_place};
+    entt::entity entities[3u]{entt::entity{3}, entt::entity{42}, traits_type::construct(9, 3)};
 
     ASSERT_EQ(set.policy(), entt::deletion_policy::in_place);
     ASSERT_TRUE(set.empty());
@@ -325,6 +377,9 @@ TEST(SparseSet, StableErase) {
 
     ASSERT_FALSE(set.empty());
     ASSERT_EQ(set.size(), 3u);
+    ASSERT_EQ(set.current(entities[0u]), traits_type::to_version(entt::tombstone));
+    ASSERT_EQ(set.current(entities[1u]), traits_type::to_version(entt::tombstone));
+    ASSERT_EQ(set.current(entities[2u]), traits_type::to_version(entt::tombstone));
     ASSERT_TRUE(set.at(0u) == entt::tombstone);
     ASSERT_TRUE(set.at(1u) == entt::tombstone);
     ASSERT_TRUE(set.at(2u) == entt::tombstone);
@@ -335,6 +390,9 @@ TEST(SparseSet, StableErase) {
 
     ASSERT_FALSE(set.empty());
     ASSERT_EQ(set.size(), 6u);
+    ASSERT_EQ(set.current(entities[0u]), traits_type::to_version(entt::tombstone));
+    ASSERT_EQ(set.current(entities[1u]), traits_type::to_version(entt::tombstone));
+    ASSERT_EQ(set.current(entities[2u]), traits_type::to_version(entities[2u]));
     ASSERT_EQ(*set.begin(), entities[2u]);
     ASSERT_TRUE(set.at(3u) == entt::tombstone);
     ASSERT_TRUE(set.at(4u) == entt::tombstone);
@@ -345,6 +403,7 @@ TEST(SparseSet, StableErase) {
     ASSERT_DEATH(set.erase(entities[2u]), "");
     ASSERT_FALSE(set.empty());
     ASSERT_EQ(set.size(), 6u);
+    ASSERT_EQ(set.current(entities[2u]), traits_type::to_version(entt::tombstone));
     ASSERT_EQ(set.slot(), 5u);
 
     set.insert(std::begin(entities), std::end(entities));
@@ -353,6 +412,7 @@ TEST(SparseSet, StableErase) {
 
     ASSERT_FALSE(set.empty());
     ASSERT_EQ(set.size(), 9u);
+    ASSERT_EQ(set.current(entities[2u]), traits_type::to_version(entities[2u]));
     ASSERT_TRUE(set.at(6u) == entt::tombstone);
     ASSERT_EQ(set.at(7u), entities[2u]);
     ASSERT_EQ(*++set.begin(), entities[2u]);
@@ -363,18 +423,25 @@ TEST(SparseSet, StableErase) {
 
     ASSERT_FALSE(set.empty());
     ASSERT_EQ(set.size(), 1u);
+    ASSERT_EQ(set.current(entities[0u]), traits_type::to_version(entt::tombstone));
+    ASSERT_EQ(set.current(entities[1u]), traits_type::to_version(entt::tombstone));
+    ASSERT_EQ(set.current(entities[2u]), traits_type::to_version(entities[2u]));
     ASSERT_EQ(*set.begin(), entities[2u]);
     ASSERT_EQ(set.slot(), 1u);
 
     set.clear();
 
     ASSERT_EQ(set.size(), 0u);
+    ASSERT_EQ(set.current(entities[2u]), traits_type::to_version(entt::tombstone));
     ASSERT_EQ(set.slot(), 0u);
 
     set.insert(std::begin(entities), std::end(entities));
     set.erase(entities[2u]);
 
     ASSERT_DEATH(set.erase(entities[2u]), "");
+    ASSERT_NE(set.current(entities[0u]), traits_type::to_version(entt::tombstone));
+    ASSERT_NE(set.current(entities[1u]), traits_type::to_version(entt::tombstone));
+    ASSERT_EQ(set.current(entities[2u]), traits_type::to_version(entt::tombstone));
     ASSERT_EQ(set.slot(), 2u);
 
     set.erase(entities[0u]);
@@ -382,6 +449,9 @@ TEST(SparseSet, StableErase) {
 
     ASSERT_DEATH(set.erase(entities, entities + 2u), "");
     ASSERT_EQ(set.size(), 3u);
+    ASSERT_EQ(set.current(entities[0u]), traits_type::to_version(entt::tombstone));
+    ASSERT_EQ(set.current(entities[1u]), traits_type::to_version(entt::tombstone));
+    ASSERT_EQ(set.current(entities[2u]), traits_type::to_version(entt::tombstone));
     ASSERT_TRUE(*set.begin() == entt::tombstone);
     ASSERT_EQ(set.slot(), 1u);
 
@@ -397,15 +467,20 @@ TEST(SparseSet, StableErase) {
     ASSERT_EQ(set.at(0u), entities[1u]);
     ASSERT_EQ(set.at(1u), entities[0u]);
     ASSERT_EQ(set.at(2u), entities[2u]);
+
+    ASSERT_NE(set.current(entities[0u]), traits_type::to_version(entt::tombstone));
+    ASSERT_NE(set.current(entities[1u]), traits_type::to_version(entt::tombstone));
+    ASSERT_NE(set.current(entities[2u]), traits_type::to_version(entt::tombstone));
+
+    ASSERT_DEATH(set.erase(traits_type::construct(9, 0)), "");
+    ASSERT_DEATH(set.erase(entt::null), "");
 }
 
 TEST(SparseSet, Remove) {
-    entt::sparse_set set;
-    entt::entity entities[3u];
+    using traits_type = entt::entt_traits<entt::entity>;
 
-    entities[0u] = entt::entity{3};
-    entities[1u] = entt::entity{42};
-    entities[2u] = entt::entity{9};
+    entt::sparse_set set;
+    entt::entity entities[3u]{entt::entity{3}, entt::entity{42}, traits_type::construct(9, 3)};
 
     ASSERT_EQ(set.policy(), entt::deletion_policy::swap_and_pop);
     ASSERT_TRUE(set.empty());
@@ -419,37 +494,50 @@ TEST(SparseSet, Remove) {
 
     ASSERT_EQ(set.remove(set.begin(), set.end()), 3u);
     ASSERT_TRUE(set.empty());
+    ASSERT_EQ(set.current(entities[0u]), traits_type::to_version(entt::tombstone));
+    ASSERT_EQ(set.current(entities[1u]), traits_type::to_version(entt::tombstone));
+    ASSERT_EQ(set.current(entities[2u]), traits_type::to_version(entt::tombstone));
 
     set.insert(std::begin(entities), std::end(entities));
 
     ASSERT_EQ(set.remove(entities, entities + 2u), 2u);
     ASSERT_FALSE(set.empty());
+    ASSERT_EQ(set.current(entities[0u]), traits_type::to_version(entt::tombstone));
+    ASSERT_EQ(set.current(entities[1u]), traits_type::to_version(entt::tombstone));
+    ASSERT_EQ(set.current(entities[2u]), traits_type::to_version(entities[2u]));
     ASSERT_EQ(*set.begin(), entities[2u]);
 
     ASSERT_EQ(set.remove(entities[2u]), 1u);
     ASSERT_EQ(set.remove(entities[2u]), 0u);
     ASSERT_TRUE(set.empty());
+    ASSERT_EQ(set.current(entities[2u]), traits_type::to_version(entt::tombstone));
 
     set.insert(entities, entities + 2u);
 
     ASSERT_EQ(set.remove(std::begin(entities), std::end(entities)), 2u);
+    ASSERT_EQ(set.current(entities[0u]), traits_type::to_version(entt::tombstone));
+    ASSERT_EQ(set.current(entities[1u]), traits_type::to_version(entt::tombstone));
+    ASSERT_EQ(set.current(entities[2u]), traits_type::to_version(entt::tombstone));
     ASSERT_TRUE(set.empty());
 
     set.insert(std::begin(entities), std::end(entities));
     std::swap(entities[1u], entities[2u]);
 
     ASSERT_EQ(set.remove(entities, entities + 2u), 2u);
+    ASSERT_EQ(set.current(entities[2u]), traits_type::to_version(entities[2u]));
     ASSERT_FALSE(set.empty());
     ASSERT_EQ(*set.begin(), entities[2u]);
+
+    ASSERT_EQ(set.remove(traits_type::construct(9, 0)), 0u);
+    ASSERT_EQ(set.remove(entt::tombstone), 0u);
+    ASSERT_EQ(set.remove(entt::null), 0u);
 }
 
 TEST(SparseSet, StableRemove) {
-    entt::sparse_set set{entt::deletion_policy::in_place};
-    entt::entity entities[3u];
+    using traits_type = entt::entt_traits<entt::entity>;
 
-    entities[0u] = entt::entity{3};
-    entities[1u] = entt::entity{42};
-    entities[2u] = entt::entity{9};
+    entt::sparse_set set{entt::deletion_policy::in_place};
+    entt::entity entities[3u]{entt::entity{3}, entt::entity{42}, traits_type::construct(9, 3)};
 
     ASSERT_EQ(set.policy(), entt::deletion_policy::in_place);
     ASSERT_TRUE(set.empty());
@@ -466,6 +554,9 @@ TEST(SparseSet, StableRemove) {
     ASSERT_EQ(set.remove(set.begin(), set.end()), 3u);
     ASSERT_FALSE(set.empty());
     ASSERT_EQ(set.size(), 3u);
+    ASSERT_EQ(set.current(entities[0u]), traits_type::to_version(entt::tombstone));
+    ASSERT_EQ(set.current(entities[1u]), traits_type::to_version(entt::tombstone));
+    ASSERT_EQ(set.current(entities[2u]), traits_type::to_version(entt::tombstone));
     ASSERT_TRUE(set.at(0u) == entt::tombstone);
     ASSERT_TRUE(set.at(1u) == entt::tombstone);
     ASSERT_TRUE(set.at(2u) == entt::tombstone);
@@ -476,7 +567,10 @@ TEST(SparseSet, StableRemove) {
     ASSERT_EQ(set.remove(entities, entities + 2u), 2u);
     ASSERT_FALSE(set.empty());
     ASSERT_EQ(set.size(), 6u);
-    ASSERT_EQ(*set.begin(), entt::entity{9});
+    ASSERT_EQ(set.current(entities[0u]), traits_type::to_version(entt::tombstone));
+    ASSERT_EQ(set.current(entities[1u]), traits_type::to_version(entt::tombstone));
+    ASSERT_EQ(set.current(entities[2u]), traits_type::to_version(entities[2u]));
+    ASSERT_EQ(*set.begin(), entities[2u]);
     ASSERT_TRUE(set.at(3u) == entt::tombstone);
     ASSERT_TRUE(set.at(4u) == entt::tombstone);
     ASSERT_EQ(set.slot(), 4u);
@@ -487,6 +581,7 @@ TEST(SparseSet, StableRemove) {
     ASSERT_EQ(set.remove(entities[2u]), 0u);
     ASSERT_FALSE(set.empty());
     ASSERT_EQ(set.size(), 6u);
+    ASSERT_EQ(set.current(entities[2u]), traits_type::to_version(entt::tombstone));
     ASSERT_TRUE(*set.begin() == entt::tombstone);
     ASSERT_EQ(set.slot(), 5u);
 
@@ -495,6 +590,9 @@ TEST(SparseSet, StableRemove) {
     ASSERT_EQ(set.remove(std::begin(entities), std::end(entities)), 2u);
     ASSERT_FALSE(set.empty());
     ASSERT_EQ(set.size(), 8u);
+    ASSERT_EQ(set.current(entities[0u]), traits_type::to_version(entt::tombstone));
+    ASSERT_EQ(set.current(entities[1u]), traits_type::to_version(entt::tombstone));
+    ASSERT_EQ(set.current(entities[2u]), traits_type::to_version(entt::tombstone));
     ASSERT_TRUE(set.at(6u) == entt::tombstone);
     ASSERT_TRUE(set.at(7u) == entt::tombstone);
     ASSERT_EQ(set.slot(), 7u);
@@ -505,6 +603,7 @@ TEST(SparseSet, StableRemove) {
     ASSERT_EQ(set.remove(entities, entities + 2u), 2u);
     ASSERT_FALSE(set.empty());
     ASSERT_EQ(set.size(), 11u);
+    ASSERT_EQ(set.current(entities[2u]), traits_type::to_version(entities[2u]));
     ASSERT_TRUE(set.at(8u) == entt::tombstone);
     ASSERT_EQ(set.at(9u), entities[2u]);
     ASSERT_EQ(*++set.begin(), entities[2u]);
@@ -515,12 +614,16 @@ TEST(SparseSet, StableRemove) {
 
     ASSERT_FALSE(set.empty());
     ASSERT_EQ(set.size(), 1u);
+    ASSERT_EQ(set.current(entities[0u]), traits_type::to_version(entt::tombstone));
+    ASSERT_EQ(set.current(entities[1u]), traits_type::to_version(entt::tombstone));
+    ASSERT_EQ(set.current(entities[2u]), traits_type::to_version(entities[2u]));
     ASSERT_EQ(*set.begin(), entities[2u]);
     ASSERT_EQ(set.slot(), 1u);
 
     set.clear();
 
     ASSERT_EQ(set.size(), 0u);
+    ASSERT_EQ(set.current(entities[2u]), traits_type::to_version(entt::tombstone));
     ASSERT_EQ(set.slot(), 0u);
 
     set.insert(std::begin(entities), std::end(entities));
@@ -528,11 +631,18 @@ TEST(SparseSet, StableRemove) {
     ASSERT_EQ(set.remove(entities[2u]), 1u);
     ASSERT_EQ(set.remove(entities[2u]), 0u);
 
+    ASSERT_NE(set.current(entities[0u]), traits_type::to_version(entt::tombstone));
+    ASSERT_NE(set.current(entities[1u]), traits_type::to_version(entt::tombstone));
+    ASSERT_EQ(set.current(entities[2u]), traits_type::to_version(entt::tombstone));
+
     ASSERT_EQ(set.remove(entities[0u]), 1u);
     ASSERT_EQ(set.remove(entities[1u]), 1u);
     ASSERT_EQ(set.remove(entities, entities + 2u), 0u);
 
     ASSERT_EQ(set.size(), 3u);
+    ASSERT_EQ(set.current(entities[0u]), traits_type::to_version(entt::tombstone));
+    ASSERT_EQ(set.current(entities[1u]), traits_type::to_version(entt::tombstone));
+    ASSERT_EQ(set.current(entities[2u]), traits_type::to_version(entt::tombstone));
     ASSERT_TRUE(*set.begin() == entt::tombstone);
     ASSERT_EQ(set.slot(), 1u);
 
@@ -548,6 +658,14 @@ TEST(SparseSet, StableRemove) {
     ASSERT_EQ(set.at(0u), entities[1u]);
     ASSERT_EQ(set.at(1u), entities[0u]);
     ASSERT_EQ(set.at(2u), entities[2u]);
+
+    ASSERT_NE(set.current(entities[0u]), traits_type::to_version(entt::tombstone));
+    ASSERT_NE(set.current(entities[1u]), traits_type::to_version(entt::tombstone));
+    ASSERT_NE(set.current(entities[2u]), traits_type::to_version(entt::tombstone));
+
+    ASSERT_EQ(set.remove(traits_type::construct(9, 0)), 0u);
+    ASSERT_EQ(set.remove(entt::tombstone), 0u);
+    ASSERT_EQ(set.remove(entt::null), 0u);
 }
 
 TEST(SparseSet, Compact) {
@@ -592,6 +710,31 @@ TEST(SparseSet, Compact) {
     ASSERT_TRUE(set.empty());
 }
 
+TEST(SparseSet, Swap) {
+    using traits_type = entt::entt_traits<entt::entity>;
+
+    entt::sparse_set set;
+
+    set.emplace(traits_type::construct(3, 5));
+    set.emplace(traits_type::construct(42, 99));
+
+    ASSERT_EQ(set.index(traits_type::construct(3, 5)), 0u);
+    ASSERT_EQ(set.index(traits_type::construct(42, 99)), 1u);
+
+    ASSERT_DEATH(set.swap(traits_type::construct(3, 5), traits_type::construct(42, 98)), "");
+    ASSERT_DEATH(set.swap(traits_type::construct(3, 6), traits_type::construct(42, 99)), "");
+
+    set.swap(traits_type::construct(3, 5), traits_type::construct(42, 99));
+
+    ASSERT_EQ(set.index(traits_type::construct(3, 5)), 1u);
+    ASSERT_EQ(set.index(traits_type::construct(42, 99)), 0u);
+
+    set.swap(traits_type::construct(3, 5), traits_type::construct(42, 99));
+
+    ASSERT_EQ(set.index(traits_type::construct(3, 5)), 0u);
+    ASSERT_EQ(set.index(traits_type::construct(42, 99)), 1u);
+}
+
 TEST(SparseSet, Clear) {
     entt::sparse_set set;
 
@@ -696,19 +839,24 @@ TEST(SparseSet, ReverseIterator) {
 }
 
 TEST(SparseSet, Find) {
+    using traits_type = entt::entt_traits<entt::entity>;
+
     entt::sparse_set set;
     set.emplace(entt::entity{3});
     set.emplace(entt::entity{42});
-    set.emplace(entt::entity{99});
+    set.emplace(traits_type::construct(99, 1));
 
     ASSERT_NE(set.find(entt::entity{3}), set.end());
     ASSERT_NE(set.find(entt::entity{42}), set.end());
-    ASSERT_NE(set.find(entt::entity{99}), set.end());
+    ASSERT_NE(set.find(traits_type::construct(99, 1)), set.end());
+    ASSERT_EQ(set.find(traits_type::construct(99, 5)), set.end());
     ASSERT_EQ(set.find(entt::entity{0}), set.end());
+    ASSERT_EQ(set.find(entt::tombstone), set.end());
+    ASSERT_EQ(set.find(entt::null), set.end());
 
-    auto it = set.find(entt::entity{99});
+    auto it = set.find(traits_type::construct(99, 1));
 
-    ASSERT_EQ(*it, entt::entity{99});
+    ASSERT_EQ(*it, traits_type::construct(99, 1));
     ASSERT_EQ(*(++it), entt::entity{42});
     ASSERT_EQ(*(++it), entt::entity{3});
     ASSERT_EQ(++it, set.end());
@@ -761,11 +909,11 @@ TEST(SparseSet, SortUnordered) {
     auto begin = set.begin();
     auto end = set.end();
 
-    ASSERT_EQ(*(begin++), entt::entity{3});
-    ASSERT_EQ(*(begin++), entt::entity{7});
-    ASSERT_EQ(*(begin++), entt::entity{9});
-    ASSERT_EQ(*(begin++), entt::entity{12});
-    ASSERT_EQ(*(begin++), entt::entity{42});
+    ASSERT_EQ(*(begin++), entities[2u]);
+    ASSERT_EQ(*(begin++), entities[1u]);
+    ASSERT_EQ(*(begin++), entities[0u]);
+    ASSERT_EQ(*(begin++), entities[3u]);
+    ASSERT_EQ(*(begin++), entities[4u]);
     ASSERT_EQ(begin, end);
 }
 
@@ -780,20 +928,20 @@ TEST(SparseSet, SortRange) {
 
     set.sort_n(2u, std::less{});
 
-    ASSERT_EQ(set.data()[0u], entt::entity{9});
-    ASSERT_EQ(set.data()[1u], entt::entity{7});
-    ASSERT_EQ(set.data()[2u], entt::entity{3});
+    ASSERT_EQ(set.data()[0u], entities[1u]);
+    ASSERT_EQ(set.data()[1u], entities[0u]);
+    ASSERT_EQ(set.data()[2u], entities[2u]);
 
     set.sort_n(5u, std::less{});
 
     auto begin = set.begin();
     auto end = set.end();
 
-    ASSERT_EQ(*(begin++), entt::entity{3});
-    ASSERT_EQ(*(begin++), entt::entity{7});
-    ASSERT_EQ(*(begin++), entt::entity{9});
-    ASSERT_EQ(*(begin++), entt::entity{12});
-    ASSERT_EQ(*(begin++), entt::entity{42});
+    ASSERT_EQ(*(begin++), entities[2u]);
+    ASSERT_EQ(*(begin++), entities[0u]);
+    ASSERT_EQ(*(begin++), entities[1u]);
+    ASSERT_EQ(*(begin++), entities[3u]);
+    ASSERT_EQ(*(begin++), entities[4u]);
     ASSERT_EQ(begin, end);
 }
 
@@ -829,9 +977,9 @@ TEST(SparseSet, RespectOverlap) {
     auto begin = lhs.begin();
     auto end = lhs.end();
 
-    ASSERT_EQ(*(begin++), entt::entity{12});
-    ASSERT_EQ(*(begin++), entt::entity{42});
-    ASSERT_EQ(*(begin++), entt::entity{3});
+    ASSERT_EQ(*(begin++), lhs_entities[1u]);
+    ASSERT_EQ(*(begin++), lhs_entities[2u]);
+    ASSERT_EQ(*(begin++), lhs_entities[0u]);
     ASSERT_EQ(begin, end);
 }
 
@@ -871,12 +1019,12 @@ TEST(SparseSet, RespectReverse) {
     auto begin = rhs.begin();
     auto end = rhs.end();
 
-    ASSERT_EQ(*(begin++), entt::entity{5});
-    ASSERT_EQ(*(begin++), entt::entity{4});
-    ASSERT_EQ(*(begin++), entt::entity{3});
-    ASSERT_EQ(*(begin++), entt::entity{2});
-    ASSERT_EQ(*(begin++), entt::entity{1});
-    ASSERT_EQ(*(begin++), entt::entity{6});
+    ASSERT_EQ(*(begin++), rhs_entities[0u]);
+    ASSERT_EQ(*(begin++), rhs_entities[1u]);
+    ASSERT_EQ(*(begin++), rhs_entities[2u]);
+    ASSERT_EQ(*(begin++), rhs_entities[3u]);
+    ASSERT_EQ(*(begin++), rhs_entities[4u]);
+    ASSERT_EQ(*(begin++), rhs_entities[5u]);
     ASSERT_EQ(begin, end);
 }
 
@@ -898,12 +1046,41 @@ TEST(SparseSet, RespectUnordered) {
     auto begin = rhs.begin();
     auto end = rhs.end();
 
-    ASSERT_EQ(*(begin++), entt::entity{5});
-    ASSERT_EQ(*(begin++), entt::entity{4});
-    ASSERT_EQ(*(begin++), entt::entity{3});
-    ASSERT_EQ(*(begin++), entt::entity{2});
-    ASSERT_EQ(*(begin++), entt::entity{1});
-    ASSERT_EQ(*(begin++), entt::entity{6});
+    ASSERT_EQ(*(begin++), rhs_entities[5u]);
+    ASSERT_EQ(*(begin++), rhs_entities[4u]);
+    ASSERT_EQ(*(begin++), rhs_entities[0u]);
+    ASSERT_EQ(*(begin++), rhs_entities[1u]);
+    ASSERT_EQ(*(begin++), rhs_entities[3u]);
+    ASSERT_EQ(*(begin++), rhs_entities[2u]);
+    ASSERT_EQ(begin, end);
+}
+
+TEST(SparseSet, RespectInvalid) {
+    using traits_type = entt::entt_traits<entt::entity>;
+
+    entt::sparse_set lhs;
+    entt::sparse_set rhs;
+
+    entt::entity lhs_entities[3u]{entt::entity{1}, entt::entity{2}, traits_type::construct(3, 1)};
+    lhs.insert(std::begin(lhs_entities), std::end(lhs_entities));
+
+    entt::entity rhs_entities[3u]{entt::entity{2}, entt::entity{1}, traits_type::construct(3, 2)};
+    rhs.insert(std::begin(rhs_entities), std::end(rhs_entities));
+
+    ASSERT_TRUE(std::equal(std::rbegin(lhs_entities), std::rend(lhs_entities), lhs.begin(), lhs.end()));
+    ASSERT_TRUE(std::equal(std::rbegin(rhs_entities), std::rend(rhs_entities), rhs.begin(), rhs.end()));
+
+    rhs.respect(lhs);
+
+    auto begin = rhs.begin();
+    auto end = rhs.end();
+
+    ASSERT_EQ(*(begin++), rhs_entities[0u]);
+    ASSERT_EQ(*(begin++), rhs_entities[1u]);
+    ASSERT_EQ(*(begin++), rhs_entities[2u]);
+    ASSERT_EQ(rhs.current(rhs_entities[0u]), 0u);
+    ASSERT_EQ(rhs.current(rhs_entities[1u]), 0u);
+    ASSERT_EQ(rhs.current(rhs_entities[2u]), 2u);
     ASSERT_EQ(begin, end);
 }