Explorar el Código

sparse_set/storage:
* added component traits (component.hpp)
* support for in-place delete (tombstones)

Michele Caini hace 4 años
padre
commit
d49e7ba4b2

+ 29 - 0
src/entt/entity/component.hpp

@@ -0,0 +1,29 @@
+#ifndef ENTT_ENTITY_COMPONENT_HPP
+#define ENTT_ENTITY_COMPONENT_HPP
+
+
+#include <type_traits>
+#include "../config/config.h"
+
+
+namespace entt {
+
+
+/**
+ * @brief Common way to access various properties of components
+ * @tparam Type Type of component.
+ */
+template<typename Type, typename = void>
+struct component_traits {
+    static_assert(std::is_same_v<std::decay_t<Type>, Type>, "Type is not decayed");
+    /*! @brief Pointer stable component, default is `std::false_type`. */
+    using in_place_delete = std::false_type;
+    /*! @brief Empty type optimization, default is `ENTT_IGNORE_IF_EMPTY`. */
+    using ignore_if_empty = ENTT_IGNORE_IF_EMPTY;
+};
+
+
+}
+
+
+#endif

+ 2 - 2
src/entt/entity/fwd.hpp

@@ -13,8 +13,8 @@ template<typename Entity, typename = std::allocator<Entity>>
 class basic_sparse_set;
 
 
-template<typename, typename Type, typename = std::allocator<Type>, typename = void>
-class basic_storage;
+template<typename, typename Type, typename = std::allocator<Type>>
+struct basic_storage;
 
 
 template<typename>

+ 166 - 101
src/entt/entity/sparse_set.hpp

@@ -17,6 +17,15 @@
 namespace entt {
 
 
+/*! @brief Sparse set deletion policy. */
+enum class deletion_policy: std::uint8_t {
+    /*! @brief Swap-and-pop deletion policy. */
+    swap_and_pop = 0u,
+    /*! @brief In-place deletion policy. */
+    in_place = 1u
+};
+
+
 /**
  * @brief Basic sparse set implementation.
  *
@@ -57,17 +66,8 @@ class basic_sparse_set {
     static_assert(alloc_traits::propagate_on_container_move_assignment::value);
     static_assert(bucket_alloc_traits::propagate_on_container_move_assignment::value);
 
-    class sparse_set_iterator final {
-        friend class basic_sparse_set<Entity, Allocator>;
-
-        using index_type = typename traits_type::difference_type;
-
-        sparse_set_iterator(const alloc_const_pointer *ref, const index_type idx) ENTT_NOEXCEPT
-            : packed{ref}, index{idx}
-        {}
-
-    public:
-        using difference_type = index_type;
+    struct sparse_set_iterator final {
+        using difference_type = typename traits_type::difference_type;
         using value_type = Entity;
         using pointer = const value_type *;
         using reference = const value_type &;
@@ -75,6 +75,11 @@ class basic_sparse_set {
 
         sparse_set_iterator() ENTT_NOEXCEPT = default;
 
+        sparse_set_iterator(const alloc_const_pointer *ref, const difference_type idx) ENTT_NOEXCEPT
+            : packed{ref},
+              index{idx}
+        {}
+
         sparse_set_iterator & operator++() ENTT_NOEXCEPT {
             return --index, *this;
         }
@@ -155,7 +160,7 @@ class basic_sparse_set {
 
     private:
         const alloc_const_pointer *packed;
-        index_type index;
+        difference_type index;
     };
 
     [[nodiscard]] static auto page(const Entity entt) ENTT_NOEXCEPT {
@@ -163,7 +168,7 @@ class basic_sparse_set {
     }
 
     [[nodiscard]] static auto offset(const Entity entt) ENTT_NOEXCEPT {
-        return size_type{traits_type::to_integral(entt) & (sparse_page - 1)};
+        return size_type{traits_type::to_entity(entt) & (sparse_page - 1)};
     }
 
     [[nodiscard]] auto assure_page(const std::size_t idx) {
@@ -171,19 +176,8 @@ class basic_sparse_set {
             const size_type sz = idx + 1u;
             const auto mem = bucket_alloc_traits::allocate(bucket_allocator, sz);
 
-            ENTT_TRY {
-                std::uninitialized_value_construct(mem + bucket, mem + sz);
-
-                ENTT_TRY {
-                    std::uninitialized_copy(sparse, sparse + bucket, mem);
-                } ENTT_CATCH {
-                    std::destroy(mem + bucket, mem + sz);
-                    ENTT_THROW;
-                }
-            } ENTT_CATCH {
-                bucket_alloc_traits::deallocate(bucket_allocator, mem, sz);
-                ENTT_THROW;
-            }
+            std::uninitialized_value_construct(mem + bucket, mem + sz);
+            std::uninitialized_copy(sparse, sparse + bucket, mem);
 
             std::destroy(sparse, sparse + bucket);
             bucket_alloc_traits::deallocate(bucket_allocator, sparse, bucket);
@@ -193,16 +187,8 @@ class basic_sparse_set {
         }
 
         if(!sparse[idx]) {
-            const auto mem = alloc_traits::allocate(allocator, sparse_page);
-
-            ENTT_TRY {
-                std::uninitialized_fill(mem, mem + sparse_page, null);
-            } ENTT_CATCH {
-                alloc_traits::deallocate(allocator, mem, sparse_page);
-                ENTT_THROW;
-            }
-
-            sparse[idx] = mem;
+            sparse[idx] = alloc_traits::allocate(allocator, sparse_page);
+            std::uninitialized_fill(sparse[idx], sparse[idx] + sparse_page, null);
         }
 
         return sparse[idx];
@@ -212,14 +198,10 @@ class basic_sparse_set {
         ENTT_ASSERT((req != reserved) && !(req < count), "Invalid request");
         const auto mem = alloc_traits::allocate(allocator, req);
 
-        ENTT_TRY {
-            std::uninitialized_copy(packed, packed + count, mem);
-        } ENTT_CATCH {
-            alloc_traits::deallocate(allocator, mem, req);
-            ENTT_THROW;
-        }
+        std::uninitialized_copy(packed, packed + count, mem);
+        std::uninitialized_fill(mem + count, mem + req, tombstone);
 
-        std::destroy(packed, packed + count);
+        std::destroy(packed, packed + reserved);
         alloc_traits::deallocate(allocator, packed, reserved);
 
         packed = mem;
@@ -235,46 +217,55 @@ class basic_sparse_set {
                 }
             }
 
-            std::destroy(packed, packed + count);
+            std::destroy(packed, packed + reserved);
             std::destroy(sparse, sparse + bucket);
             alloc_traits::deallocate(allocator, packed, reserved);
             bucket_alloc_traits::deallocate(bucket_allocator, sparse, bucket);
         }
     }
 
-    void push_back(const Entity entt) {
-        ENTT_ASSERT(count != reserved, "No more space left");
-        alloc_traits::construct(allocator, std::addressof(packed[count]), entt);
+    void stable_erase(const Entity entt, void *ud = nullptr) {
+        // last chance to use the entity for derived classes and mixins, if any
+        about_to_pop(entt, ud);
 
-        ENTT_TRY {
-            assure_page(page(entt))[offset(entt)] = entity_type{static_cast<typename traits_type::entity_type>(count)};
-        } ENTT_CATCH {
-            alloc_traits::destroy(allocator, std::addressof(packed[count]));
-            ENTT_THROW;
-        }
+        auto &ref = sparse[page(entt)][offset(entt)];
+        const auto pos = size_type{traits_type::to_integral(ref)};
+        ENTT_ASSERT(packed[pos] == entt, "Invalid entity identifier");
+
+        packed[pos] = std::exchange(free_list, (tombstone | entity_type{static_cast<typename traits_type::entity_type>(pos)}));
+        ref = null;
 
-        ++count;
+        // strong exception guarantee
+        in_place_pop(pos);
     }
 
-    void pop(const Entity entt, void *ud) {
-        ENTT_ASSERT(contains(entt), "Set does not contain entity");
+    void unstable_erase(const Entity entt, void *ud = nullptr) {
         // last chance to use the entity for derived classes and mixins, if any
-        about_to_erase(entt, ud);
+        about_to_pop(entt, ud);
 
         auto &ref = sparse[page(entt)][offset(entt)];
         const auto pos = size_type{traits_type::to_integral(ref)};
+        ENTT_ASSERT(packed[pos] == entt, "Invalid entity identifier");
+
         auto &last = packed[count - 1u];
 
-        // basic no-leak guarantee (with invalid state) if copy assignment operators throw
-        std::swap(sparse[page(last)][offset(last)], ref);
-        // swapping with entt isn't strictly required but it prevents nasty bugs
-        std::swap(packed[pos], last);
-        // no risks when pos == count, accessing packed is no longer required
+        packed[pos] = last;
+        sparse[page(last)][offset(last)] = ref;
+        // lazy self-assignment guard
         ref = null;
+        // unnecessary but it helps to detect nasty bugs
+        ENTT_ASSERT((last = tombstone, true), "");
+
+        --count;
 
-        // don't expect exceptions here, instead allow for nosy destructors
-        alloc_traits::destroy(allocator, std::addressof(packed[--count]));
-        swap_and_pop(pos);
+        ENTT_TRY {
+            // strong exception guarantee
+            swap_and_pop(pos);
+        } ENTT_CATCH {
+            last = std::exchange(packed[pos], entt);
+            ref = std::exchange(sparse[page(last)][offset(last)], entity_type{static_cast<typename traits_type::entity_type>(count++)});
+            ENTT_THROW;
+        }
     }
 
 protected:
@@ -285,18 +276,31 @@ protected:
      */
     virtual void swap_at([[maybe_unused]] const std::size_t lhs, [[maybe_unused]] const std::size_t rhs) {}
 
+    /**
+     * @brief Attempts to move an entity in the internal packed array.
+     * @param from A valid position of an entity within storage.
+     * @param to A valid position of an entity within storage.
+     */
+    virtual void move_and_pop([[maybe_unused]] const std::size_t from, [[maybe_unused]] const std::size_t to) {}
+
     /**
      * @brief Attempts to erase an entity from the internal packed array.
      * @param pos A valid position of an entity within storage.
      */
     virtual void swap_and_pop([[maybe_unused]] const std::size_t pos) {}
 
+    /**
+     * @brief Attempts to erase an entity from the internal packed array.
+     * @param pos A valid position of an entity within storage.
+     */
+    virtual void in_place_pop([[maybe_unused]] const std::size_t pos) {}
+
     /**
      * @brief Last chance to use an entity that is about to be erased.
      * @param entity A valid entity identifier.
      * @param ud Optional user data that are forwarded as-is to derived classes.
      */
-    virtual void about_to_erase([[maybe_unused]] const Entity entity, [[maybe_unused]] void *ud) {}
+    virtual void about_to_pop([[maybe_unused]] const Entity entity, [[maybe_unused]] void *ud) {}
 
 public:
     /*! @brief Allocator type. */
@@ -310,20 +314,31 @@ public:
     /*! @brief Random access iterator type. */
     using iterator = sparse_set_iterator;
     /*! @brief Reverse iterator type. */
-    using reverse_iterator = pointer;
+    using reverse_iterator = std::reverse_iterator<iterator>;
 
     /**
-     * @brief Default constructor.
+     * @brief Constructs an empty container with the given policy and allocator.
+     * @param pol Type of deletion policy.
      * @param alloc Allocator to use (possibly default-constructed).
      */
-    explicit basic_sparse_set(const allocator_type &alloc = {})
+    explicit basic_sparse_set(deletion_policy pol, const allocator_type &alloc = {})
         : allocator{alloc},
           bucket_allocator{alloc},
           sparse{bucket_alloc_traits::allocate(bucket_allocator, 0u)},
           packed{alloc_traits::allocate(allocator, 0u)},
           bucket{0u},
           count{0u},
-          reserved{0u}
+          reserved{0u},
+          free_list{tombstone},
+          policy{pol}
+    {}
+
+    /**
+     * @brief Constructs an empty container with the given allocator.
+     * @param alloc Allocator to use (possibly default-constructed).
+     */
+    explicit basic_sparse_set(const allocator_type &alloc = {})
+        : basic_sparse_set{deletion_policy::swap_and_pop, alloc}
     {}
 
     /**
@@ -337,7 +352,9 @@ public:
           packed{std::exchange(other.packed, alloc_pointer{})},
           bucket{std::exchange(other.bucket, 0u)},
           count{std::exchange(other.count, 0u)},
-          reserved{std::exchange(other.reserved, 0u)}
+          reserved{std::exchange(other.reserved, 0u)},
+          free_list{std::exchange(other.free_list, tombstone)},
+          policy{other.policy}
     {}
 
     /*! @brief Default destructor. */
@@ -360,6 +377,8 @@ public:
         bucket = std::exchange(other.bucket, 0u);
         count = std::exchange(other.count, 0u);
         reserved = std::exchange(other.reserved, 0u);
+        free_list = std::exchange(other.free_list, tombstone);
+        policy = other.policy;
 
         return *this;
     }
@@ -476,7 +495,7 @@ public:
      * array.
      */
     [[nodiscard]] reverse_iterator rbegin() const ENTT_NOEXCEPT {
-        return data();
+        return std::make_reverse_iterator(end());
     }
 
     /**
@@ -490,7 +509,7 @@ public:
      * reversed internal packed array.
      */
     [[nodiscard]] reverse_iterator rend() const ENTT_NOEXCEPT {
-        return rbegin() + count;
+        return std::make_reverse_iterator(begin());
     }
 
     /**
@@ -499,7 +518,7 @@ public:
      * @return An iterator to the given entity if it's found, past the end
      * iterator otherwise.
      */
-    [[nodiscard]] iterator find(const entity_type entt) const {
+    [[nodiscard]] iterator find(const entity_type entt) const ENTT_NOEXCEPT {
         return contains(entt) ? --(end() - index(entt)) : end();
     }
 
@@ -508,9 +527,9 @@ public:
      * @param entt A valid entity identifier.
      * @return True if the sparse set contains the entity, false otherwise.
      */
-    [[nodiscard]] bool contains(const entity_type entt) const {
+    [[nodiscard]] bool contains(const entity_type entt) const ENTT_NOEXCEPT {
         const auto curr = page(entt);
-        // testing against null permits to avoid accessing the packed array
+        // testing versions permits to avoid accessing the packed array
         return (curr < bucket && sparse[curr] && sparse[curr][offset(entt)] != null);
     }
 
@@ -524,7 +543,7 @@ public:
      * @param entt A valid entity identifier.
      * @return The position of the entity in the sparse set.
      */
-    [[nodiscard]] size_type index(const entity_type entt) const {
+    [[nodiscard]] size_type index(const entity_type entt) const ENTT_NOEXCEPT {
         ENTT_ASSERT(contains(entt), "Set does not contain entity");
         return size_type{traits_type::to_integral(sparse[page(entt)][offset(entt)])};
     }
@@ -534,7 +553,7 @@ public:
      * @param pos The position for which to return the entity.
      * @return The entity at specified location if any, a null entity otherwise.
      */
-    [[nodiscard]] entity_type at(const size_type pos) const {
+    [[nodiscard]] entity_type at(const size_type pos) const ENTT_NOEXCEPT {
         return pos < count ? packed[pos] : null;
     }
 
@@ -543,7 +562,7 @@ public:
      * @param pos The position for which to return the entity.
      * @return The entity at specified location.
      */
-    [[nodiscard]] entity_type operator[](const size_type pos) const {
+    [[nodiscard]] entity_type operator[](const size_type pos) const ENTT_NOEXCEPT {
         ENTT_ASSERT(pos < count, "Position is out of bounds");
         return packed[pos];
     }
@@ -560,12 +579,20 @@ public:
     void emplace(const entity_type entt) {
         ENTT_ASSERT(!contains(entt), "Set already contains entity");
 
-        if(count == reserved) {
-            const size_type sz = static_cast<size_type>(reserved * growth_factor);
-            resize_packed(sz + !(sz > reserved));
-        }
+        if(free_list == null) {
+            if(count == reserved) {
+                const size_type sz = static_cast<size_type>(reserved * growth_factor);
+                resize_packed(sz + !(sz > reserved));
+            }
 
-        push_back(entt);
+            assure_page(page(entt))[offset(entt)] = entity_type{static_cast<typename traits_type::entity_type>(count)};
+            packed[count++] = entt;
+        } else {
+            const auto pos = size_type{traits_type::to_entity(free_list)};
+            move_and_pop(count, pos);
+            sparse[page(entt)][offset(entt)] = entity_type{static_cast<typename traits_type::entity_type>(pos)};
+            free_list = std::exchange(packed[pos], entt);
+        }
     }
 
     /**
@@ -584,8 +611,10 @@ public:
         reserve(count + std::distance(first, last));
 
         for(; first != last; ++first) {
-            ENTT_ASSERT(!contains(*first), "Set already contains entity");
-            push_back(*first);
+            const auto entt = *first;
+            ENTT_ASSERT(!contains(entt), "Set already contains entity");
+            assure_page(page(entt))[offset(entt)] = entity_type{static_cast<typename traits_type::entity_type>(count)};
+            packed[count++] = entt;
         }
     }
 
@@ -600,11 +629,15 @@ public:
      * @param ud Optional user data that are forwarded as-is to derived classes.
      */
     void erase(const entity_type entt, void *ud = nullptr) {
-        pop(entt, ud);
+        ENTT_ASSERT(contains(entt), "Set does not contain entity");
+        (policy == deletion_policy::in_place) ? stable_erase(entt, ud) : unstable_erase(entt, ud);
     }
 
     /**
-     * @brief Erases multiple entities from a set.
+     * @brief Erases entities from a set.
+     *
+     * @sa erase
+     *
      * @tparam It Type of input iterator.
      * @param first An iterator to the first element of the range of entities.
      * @param last An iterator past the last element of the range of entities.
@@ -613,7 +646,7 @@ public:
     template<typename It>
     void erase(It first, It last, void *ud = nullptr) {
         for(; first != last; ++first) {
-            pop(*first, ud);
+            erase(*first, ud);
         }
     }
 
@@ -624,11 +657,16 @@ public:
      * @return True if the entity is actually removed, false otherwise.
      */
     bool remove(const entity_type entt, void *ud = nullptr) {
-        return contains(entt) ? (pop(entt, ud), true) : false;
+        if(contains(entt)) {
+            erase(entt, ud);
+            return true;
+        }
+
+        return false;
     }
 
     /**
-     * @brief Removes multiple entities from a sparse set if they exist.
+     * @brief Removes entities from a sparse set if they exist.
      * @tparam It Type of input iterator.
      * @param first An iterator to the first element of the range of entities.
      * @param last An iterator past the last element of the range of entities.
@@ -646,6 +684,26 @@ public:
         return found;
     }
 
+    /*! @brief Removes all tombstones from the packed array of a sparse set. */
+    void compact() {
+        size_type next = count;
+        for(; next && packed[next - 1u] == tombstone; --next);
+
+        for(auto *it = &free_list; *it != null && next; it = std::addressof(packed[traits_type::to_entity(*it)])) {
+            if(const size_type pos = traits_type::to_entity(*it); pos < next) {
+                --next;
+                move_and_pop(next, pos);
+                std::swap(packed[next], packed[pos]);
+                sparse[page(packed[pos])][offset(packed[pos])] = entity_type{static_cast<const typename traits_type::entity_type>(pos)};
+                *it = (tombstone | entity_type{static_cast<typename traits_type::entity_type>(next)});
+                for(; next && packed[next - 1u] == tombstone; --next);
+            }
+        }
+
+        free_list = tombstone;
+        count = next;
+    }
+
     /**
      * @copybrief swap_at
      *
@@ -703,6 +761,8 @@ public:
     void sort_n(const size_type length, Compare compare, Sort algo = Sort{}, Args &&... args) {
         // basic no-leak guarantee (with invalid state) if sorting throws
         ENTT_ASSERT(!(length > count), "Length exceeds the number of elements");
+        compact();
+
         algo(std::make_reverse_iterator(packed + length), std::make_reverse_iterator(packed), std::move(compare), std::forward<Args>(args)...);
 
         for(size_type pos{}; pos < length; ++pos) {
@@ -715,9 +775,7 @@ public:
 
                 swap_at(next, idx);
                 sparse[page(entt)][offset(entt)] = entity_type{static_cast<typename traits_type::entity_type>(curr)};
-
-                curr = next;
-                next = idx;
+                curr = std::exchange(next, idx);
             }
         }
     }
@@ -755,12 +813,12 @@ public:
      * @param other The sparse sets that imposes the order of the entities.
      */
     void respect(const basic_sparse_set &other) {
+        compact();
+
         const auto to = other.end();
         auto from = other.begin();
 
-        size_type pos = count - 1;
-
-        while(pos && from != to) {
+        for(size_type pos = count - 1; pos && from != to; ++from) {
             if(contains(*from)) {
                 if(*from != packed[pos]) {
                     // basic no-leak guarantee (with invalid state) if swapping throws
@@ -769,8 +827,6 @@ public:
 
                 --pos;
             }
-
-            ++from;
         }
     }
 
@@ -778,8 +834,15 @@ public:
      * @brief Clears a sparse set.
      * @param ud Optional user data that are forwarded as-is to derived classes.
      */
-    void clear(void *ud = nullptr) ENTT_NOEXCEPT {
-        erase(begin(), end(), ud);
+    void clear(void *ud = nullptr) {
+        for(auto &&entity: *this) {
+            if(entity != tombstone) {
+                stable_erase(entity, ud);
+            }
+        }
+
+        free_list = tombstone;
+        count = 0u;
     }
 
 private:
@@ -790,6 +853,8 @@ private:
     std::size_t bucket;
     std::size_t count;
     std::size_t reserved;
+    entity_type free_list;
+    deletion_policy policy;
 };
 
 

+ 82 - 64
src/entt/entity/storage.hpp

@@ -13,6 +13,7 @@
 #include "../core/fwd.hpp"
 #include "../core/type_traits.hpp"
 #include "../signal/sigh.hpp"
+#include "component.hpp"
 #include "entity.hpp"
 #include "fwd.hpp"
 #include "sparse_set.hpp"
@@ -47,14 +48,14 @@ namespace entt {
  * @tparam Type Type of objects assigned to the entities.
  * @tparam Allocator Type of allocator used to manage memory and elements.
  */
-template<typename Entity, typename Type, typename Allocator, typename>
-class basic_storage: public basic_sparse_set<Entity, typename std::allocator_traits<Allocator>::template rebind_alloc<Entity>> {
-    static_assert(std::is_move_constructible_v<Type> && std::is_move_assignable_v<Type>, "The managed type must be at least move constructible and assignable");
-
+template<typename Entity, typename Type, typename Allocator, typename = void>
+class basic_storage_impl: public basic_sparse_set<Entity, typename std::allocator_traits<Allocator>::template rebind_alloc<Entity>> {
     static constexpr auto packed_page = ENTT_PACKED_PAGE;
 
+    using comp_traits = component_traits<Type>;
+
     using underlying_type = basic_sparse_set<Entity, typename std::allocator_traits<Allocator>::template rebind_alloc<Entity>>;
-    using traits_type = entt_traits<Entity>;
+    using difference_type = typename entt_traits<Entity>::difference_type;
 
     using alloc_traits = typename std::allocator_traits<Allocator>::template rebind_traits<Type>;
     using alloc_pointer = typename alloc_traits::pointer;
@@ -70,15 +71,8 @@ class basic_storage: public basic_sparse_set<Entity, typename std::allocator_tra
     static_assert(bucket_alloc_traits::propagate_on_container_move_assignment::value);
 
     template<typename Value>
-    class storage_iterator final {
-        friend class basic_storage<Entity, Type, Allocator>;
-
-        storage_iterator(bucket_alloc_pointer const *ref, const typename traits_type::difference_type idx) ENTT_NOEXCEPT
-            : packed{ref}, index{idx}
-        {}
-
-    public:
-        using difference_type = typename traits_type::difference_type;
+    struct storage_iterator final {
+        using difference_type = typename basic_storage_impl::difference_type;
         using value_type = Value;
         using pointer = value_type *;
         using reference = value_type &;
@@ -86,6 +80,11 @@ class basic_storage: public basic_sparse_set<Entity, typename std::allocator_tra
 
         storage_iterator() ENTT_NOEXCEPT = default;
 
+        storage_iterator(bucket_alloc_pointer const *ref, const typename basic_storage_impl::difference_type idx) ENTT_NOEXCEPT
+            : packed{ref},
+              index{idx}
+        {}
+
         storage_iterator & operator++() ENTT_NOEXCEPT {
             return --index, *this;
         }
@@ -179,13 +178,10 @@ class basic_storage: public basic_sparse_set<Entity, typename std::allocator_tra
 
     void release_memory() {
         if(packed) {
-            for(size_type pos{}; pos < bucket; ++pos) {
-                if(auto length = underlying_type::size(); length) {
-                    const auto sz = length > packed_page ? packed_page : length;
-                    std::destroy(packed[pos], packed[pos] + sz);
-                    length -= sz;
-                }
+            // no-throw stable erase iteration
+            underlying_type::clear();
 
+            for(size_type pos{}; pos < bucket; ++pos) {
                 alloc_traits::deallocate(allocator, packed[pos], packed_page);
                 bucket_alloc_traits::destroy(bucket_allocator, std::addressof(packed[pos]));
             }
@@ -201,31 +197,20 @@ class basic_storage: public basic_sparse_set<Entity, typename std::allocator_tra
             const auto mem = bucket_alloc_traits::allocate(bucket_allocator, length);
 
             if(bucket > length) {
-                ENTT_TRY {
-                    std::uninitialized_copy(packed, packed + length, mem);
-                } ENTT_CATCH {
-                    bucket_alloc_traits::deallocate(bucket_allocator, mem, length);
-                    ENTT_THROW;
-                }
+                std::uninitialized_copy(packed, packed + length, mem);
 
                 for(auto pos = length; pos < bucket; ++pos) {
                     alloc_traits::deallocate(allocator, packed[pos], packed_page);
                 }
             } else {
+                std::uninitialized_copy(packed, packed + bucket, mem);
+
                 size_type pos{};
 
                 ENTT_TRY {
-                    std::uninitialized_copy(packed, packed + bucket, mem);
-
                     for(pos = bucket; pos < length; ++pos) {
                         auto pg = alloc_traits::allocate(allocator, packed_page);
-
-                        ENTT_TRY {
-                            bucket_alloc_traits::construct(bucket_allocator, std::addressof(mem[pos]), pg);
-                        } ENTT_CATCH {
-                            alloc_traits::deallocate(allocator, pg, packed_page);
-                            ENTT_THROW;
-                        }
+                        bucket_alloc_traits::construct(bucket_allocator, std::addressof(mem[pos]), pg);
                     }
                 } ENTT_CATCH {
                     for(auto next = bucket; next < pos; ++next) {
@@ -247,7 +232,7 @@ class basic_storage: public basic_sparse_set<Entity, typename std::allocator_tra
     }
 
     template<typename... Args>
-    Type & push_back(Args &&... args) {
+    auto & push_back(Args &&... args) {
         const auto length = underlying_type::size();
         ENTT_ASSERT(length < (bucket * packed_page), "No more space left");
         auto *instance = std::addressof(packed[page(length)][offset(length)]);
@@ -271,24 +256,31 @@ protected:
         std::swap(packed[page(lhs)][offset(lhs)], packed[page(rhs)][offset(rhs)]);
     }
 
+    /*! @copydoc basic_sparse_set::move_and_pop */
+    void move_and_pop(const std::size_t from, const std::size_t to) final {
+        auto *instance = std::addressof(packed[page(to)][offset(to)]);
+        auto *other = std::addressof(packed[page(from)][offset(from)]);
+        alloc_traits::construct(allocator, instance, std::move(*other));
+        alloc_traits::destroy(allocator, other);
+    }
+
     /*! @copydoc basic_sparse_set::swap_and_pop */
     void swap_and_pop(const std::size_t pos) final {
         const auto length = underlying_type::size();
         auto &&elem = packed[page(pos)][offset(pos)];
         auto &&last = packed[page(length)][offset(length)];
 
-        ENTT_TRY {
-            [[maybe_unused]] auto other = std::move(elem);
-            elem = std::move(last);
-        } ENTT_CATCH {
-            // basic no-leak guarantee (with invalid state) if swapping throws
-            alloc_traits::destroy(allocator, std::addressof(last));
-            ENTT_THROW;
-        }
-
+        // support for nosy destructors
+        [[maybe_unused]] auto unused = std::move(elem);
+        elem = std::move(last);
         alloc_traits::destroy(allocator, std::addressof(last));
     }
 
+    /*! @copydoc basic_sparse_set::in_place_pop */
+    void in_place_pop(const std::size_t pos) final {
+        alloc_traits::destroy(allocator, std::addressof(packed[page(pos)][offset(pos)]));
+    }
+
 public:
     /*! @brief Allocator type. */
     using allocator_type = typename alloc_traits::allocator_type;
@@ -303,9 +295,9 @@ public:
     /*! @brief Constant pointer type to contained elements. */
     using const_pointer = bucket_alloc_const_pointer;
     /*! @brief Random access iterator type. */
-    using iterator = storage_iterator<Type>;
+    using iterator = storage_iterator<value_type>;
     /*! @brief Constant random access iterator type. */
-    using const_iterator = storage_iterator<const Type>;
+    using const_iterator = storage_iterator<const value_type>;
     /*! @brief Reverse iterator type. */
     using reverse_iterator = std::reverse_iterator<iterator>;
     /*! @brief Constant reverse iterator type. */
@@ -315,11 +307,11 @@ public:
      * @brief Default constructor.
      * @param alloc Allocator to use (possibly default-constructed).
      */
-    explicit basic_storage(const allocator_type &alloc = {})
-        : underlying_type{alloc},
+    explicit basic_storage_impl(const allocator_type &alloc = {})
+        : underlying_type{deletion_policy{comp_traits::in_place_delete::value}, alloc},
           allocator{alloc},
           bucket_allocator{alloc},
-          packed{},
+          packed{bucket_alloc_traits::allocate(bucket_allocator, 0u)},
           bucket{}
     {}
 
@@ -327,7 +319,7 @@ public:
      * @brief Move constructor.
      * @param other The instance to move from.
      */
-    basic_storage(basic_storage &&other) ENTT_NOEXCEPT
+    basic_storage_impl(basic_storage_impl &&other) ENTT_NOEXCEPT
         : underlying_type{std::move(other)},
           allocator{std::move(other.allocator)},
           bucket_allocator{std::move(other.bucket_allocator)},
@@ -336,7 +328,7 @@ public:
     {}
 
     /*! @brief Default destructor. */
-    ~basic_storage() override {
+    ~basic_storage_impl() override {
         release_memory();
     }
 
@@ -345,7 +337,7 @@ public:
      * @param other The instance to move from.
      * @return This sparse set.
      */
-    basic_storage & operator=(basic_storage &&other) ENTT_NOEXCEPT {
+    basic_storage_impl & operator=(basic_storage_impl &&other) ENTT_NOEXCEPT {
         release_memory();
 
         underlying_type::operator=(std::move(other));
@@ -411,7 +403,7 @@ public:
      * @return An iterator to the first instance of the internal array.
      */
     [[nodiscard]] const_iterator cbegin() const ENTT_NOEXCEPT {
-        const typename traits_type::difference_type pos = underlying_type::size();
+        const difference_type pos = underlying_type::size();
         return const_iterator{std::addressof(packed), pos};
     }
 
@@ -422,7 +414,7 @@ public:
 
     /*! @copydoc begin */
     [[nodiscard]] iterator begin() ENTT_NOEXCEPT {
-        const typename traits_type::difference_type pos = underlying_type::size();
+        const difference_type pos = underlying_type::size();
         return iterator{std::addressof(packed), pos};
     }
 
@@ -507,13 +499,13 @@ public:
      * @param entt A valid entity identifier.
      * @return The object assigned to the entity.
      */
-    [[nodiscard]] const value_type & get(const entity_type entt) const {
+    [[nodiscard]] const value_type & get(const entity_type entt) const ENTT_NOEXCEPT {
         const auto idx = underlying_type::index(entt);
         return packed[page(idx)][offset(idx)];
     }
 
     /*! @copydoc get */
-    [[nodiscard]] value_type & get(const entity_type entt) {
+    [[nodiscard]] value_type & get(const entity_type entt) ENTT_NOEXCEPT {
         return const_cast<value_type &>(std::as_const(*this).get(entt));
     }
 
@@ -696,12 +688,18 @@ private:
 };
 
 
-/*! @copydoc basic_storage */
+/*! @copydoc basic_storage_impl */
 template<typename Entity, typename Type, typename Allocator>
-class basic_storage<Entity, Type, Allocator, std::enable_if_t<std::conjunction_v<ENTT_IGNORE_IF_EMPTY, std::is_empty<Type>>>>: public basic_sparse_set<Entity, typename std::allocator_traits<Allocator>::template rebind_alloc<Entity>> {
+class basic_storage_impl<Entity, Type, Allocator, std::enable_if_t<component_traits<Type>::ignore_if_empty::value && std::is_empty_v<Type>>>
+    : public basic_sparse_set<Entity, typename std::allocator_traits<Allocator>::template rebind_alloc<Entity>>
+{
+    using comp_traits = component_traits<Type>;
     using underlying_type = basic_sparse_set<Entity, typename std::allocator_traits<Allocator>::template rebind_alloc<Entity>>;
+    using alloc_traits = typename std::allocator_traits<Allocator>::template rebind_traits<Type>;
 
 public:
+    /*! @brief Allocator type. */
+    using allocator_type = typename alloc_traits::allocator_type;
     /*! @brief Type of the objects assigned to entities. */
     using value_type = Type;
     /*! @brief Underlying entity identifier. */
@@ -709,6 +707,14 @@ public:
     /*! @brief Unsigned integer type. */
     using size_type = std::size_t;
 
+    /**
+     * @brief Default constructor.
+     * @param alloc Allocator to use (possibly default-constructed).
+     */
+    explicit basic_storage_impl(const allocator_type &alloc = {})
+        : underlying_type{deletion_policy{comp_traits::in_place_delete::value}, alloc}
+    {}
+
     /**
      * @brief Fake get function.
      *
@@ -718,7 +724,7 @@ public:
      *
      * @param entt A valid entity identifier.
      */
-    void get([[maybe_unused]] const entity_type entt) const {
+    void get([[maybe_unused]] const entity_type entt) const ENTT_NOEXCEPT {
         ENTT_ASSERT(underlying_type::contains(entt), "Storage does not contain entity");
     }
 
@@ -832,11 +838,11 @@ struct storage_adapter_mixin: Type {
  */
 template<typename Type>
 class sigh_storage_mixin final: public Type {
-    /*! @copydoc basic_sparse_set::about_to_erase */
-    void about_to_erase(const typename Type::entity_type entity, void *ud) final {
+    /*! @copydoc basic_sparse_set::about_to_pop */
+    void about_to_pop(const typename Type::entity_type entity, void *ud) final {
         ENTT_ASSERT(ud != nullptr, "Invalid pointer to registry");
         destruction.publish(*static_cast<basic_registry<typename Type::entity_type> *>(ud), entity);
-        Type::about_to_erase(entity, ud);
+        Type::about_to_pop(entity, ud);
     }
 
 public:
@@ -973,9 +979,21 @@ private:
 
 
 /**
- * @brief Defines the component-to-storage conversion.
+ * @brief Storage implementation dispatcher.
  * @tparam Entity A valid entity type (see entt_traits for more details).
  * @tparam Type Type of objects assigned to the entities.
+ * @tparam Allocator Type of allocator used to manage memory and elements.
+ */
+template<typename Entity, typename Type, typename Allocator>
+struct basic_storage: basic_storage_impl<Entity, Type, Allocator> {
+    using basic_storage_impl<Entity, Type, Allocator>::basic_storage_impl;
+};
+
+
+/**
+ * @brief Provides a common way to access certain properties of storage types.
+ * @tparam Entity A valid entity type (see entt_traits for more details).
+ * @tparam Type Type of objects managed by the storage class.
  */
 template<typename Entity, typename Type, typename = void>
 struct storage_traits {

+ 1 - 0
src/entt/entt.hpp

@@ -9,6 +9,7 @@
 #include "core/type_info.hpp"
 #include "core/type_traits.hpp"
 #include "core/utility.hpp"
+#include "entity/component.hpp"
 #include "entity/entity.hpp"
 #include "entity/group.hpp"
 #include "entity/handle.hpp"

+ 324 - 77
test/entt/entity/sparse_set.cpp

@@ -5,10 +5,10 @@
 #include <functional>
 #include <type_traits>
 #include <gtest/gtest.h>
+#include <entt/entity/entity.hpp>
 #include <entt/entity/sparse_set.hpp>
 #include <entt/entity/fwd.hpp>
 #include "throwing_allocator.hpp"
-#include "throwing_entity.hpp"
 
 struct empty_type {};
 struct boxed_int { int value; };
@@ -73,6 +73,50 @@ TEST(SparseSet, Functionalities) {
     ASSERT_FALSE(set.contains(entt::entity{42}));
 }
 
+TEST(SparseSet, Contains) {
+    using traits_type = entt::entt_traits<entt::entity>;
+
+    entt::sparse_set set{entt::deletion_policy::in_place};
+
+    ASSERT_FALSE(set.contains(entt::null));
+    ASSERT_FALSE(set.contains(entt::tombstone));
+
+    set.emplace(entt::entity{0});
+    set.emplace(entt::entity{3});
+    set.emplace(entt::entity{42});
+    set.emplace(entt::entity{99});
+
+    set.emplace(entt::entity{1});
+
+    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}));
+
+    set.erase(entt::entity{0});
+    set.erase(entt::entity{3});
+
+    set.remove(entt::entity{42});
+    set.remove(entt::entity{99});
+
+    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::null));
+    ASSERT_FALSE(set.contains(entt::tombstone));
+
+    // tombstones and null entities can trigger false positives
+    ASSERT_TRUE(set.contains(traits_type::to_type(entt::entity{1}, entt::null)));
+    ASSERT_TRUE(set.contains(traits_type::to_type(entt::entity{1}, entt::tombstone)));
+
+    ASSERT_FALSE(set.contains(traits_type::to_type(entt::null, entt::entity{1})));
+    ASSERT_FALSE(set.contains(traits_type::to_type(entt::tombstone, entt::entity{1})));
+}
+
 TEST(SparseSet, Move) {
     entt::sparse_set set;
     set.emplace(entt::entity{42});
@@ -141,18 +185,18 @@ TEST(SparseSet, Pagination) {
 
 TEST(SparseSet, Insert) {
     entt::sparse_set set;
-    entt::entity entities[2];
+    entt::entity entities[2u];
 
-    entities[0] = entt::entity{3};
-    entities[1] = entt::entity{42};
+    entities[0u] = entt::entity{3};
+    entities[1u] = entt::entity{42};
 
     set.emplace(entt::entity{12});
     set.insert(std::end(entities), std::end(entities));
     set.insert(std::begin(entities), std::end(entities));
     set.emplace(entt::entity{24});
 
-    ASSERT_TRUE(set.contains(entities[0]));
-    ASSERT_TRUE(set.contains(entities[1]));
+    ASSERT_TRUE(set.contains(entities[0u]));
+    ASSERT_TRUE(set.contains(entities[1u]));
     ASSERT_FALSE(set.contains(entt::entity{0}));
     ASSERT_FALSE(set.contains(entt::entity{9}));
     ASSERT_TRUE(set.contains(entt::entity{12}));
@@ -161,27 +205,27 @@ TEST(SparseSet, Insert) {
     ASSERT_FALSE(set.empty());
     ASSERT_EQ(set.size(), 4u);
     ASSERT_EQ(set.index(entt::entity{12}), 0u);
-    ASSERT_EQ(set.index(entities[0]), 1u);
-    ASSERT_EQ(set.index(entities[1]), 2u);
+    ASSERT_EQ(set.index(entities[0u]), 1u);
+    ASSERT_EQ(set.index(entities[1u]), 2u);
     ASSERT_EQ(set.index(entt::entity{24}), 3u);
     ASSERT_EQ(set.data()[set.index(entt::entity{12})], entt::entity{12});
-    ASSERT_EQ(set.data()[set.index(entities[0])], entities[0]);
-    ASSERT_EQ(set.data()[set.index(entities[1])], entities[1]);
+    ASSERT_EQ(set.data()[set.index(entities[0u])], entities[0u]);
+    ASSERT_EQ(set.data()[set.index(entities[1u])], entities[1u]);
     ASSERT_EQ(set.data()[set.index(entt::entity{24})], entt::entity{24});
 }
 
 TEST(SparseSet, Erase) {
     entt::sparse_set set;
-    entt::entity entities[3];
+    entt::entity entities[3u];
 
-    entities[0] = entt::entity{3};
-    entities[1] = entt::entity{42};
-    entities[2] = entt::entity{9};
+    entities[0u] = entt::entity{3};
+    entities[1u] = entt::entity{42};
+    entities[2u] = entt::entity{9};
 
     ASSERT_TRUE(set.empty());
 
     ASSERT_DEATH(set.erase(std::begin(entities), std::end(entities)), "");
-    ASSERT_DEATH(set.erase(entities[1]), "");
+    ASSERT_DEATH(set.erase(entities[1u]), "");
 
     ASSERT_TRUE(set.empty());
 
@@ -194,33 +238,122 @@ TEST(SparseSet, Erase) {
     set.erase(entities, entities + 2u);
 
     ASSERT_FALSE(set.empty());
-    ASSERT_EQ(*set.begin(), entt::entity{9});
+    ASSERT_EQ(*set.begin(), entities[2u]);
 
-    set.erase(entities[2]);
+    set.erase(entities[2u]);
 
-    ASSERT_DEATH(set.erase(entities[2]), "");
+    ASSERT_DEATH(set.erase(entities[2u]), "");
     ASSERT_TRUE(set.empty());
 
     set.insert(std::begin(entities), std::end(entities));
-    std::swap(entities[1], entities[2]);
+    std::swap(entities[1u], entities[2u]);
     set.erase(entities, entities + 2u);
 
     ASSERT_FALSE(set.empty());
-    ASSERT_EQ(*set.begin(), entt::entity{42});
+    ASSERT_EQ(*set.begin(), entities[2u]);
+}
+
+TEST(SparseSet, StableErase) {
+    entt::sparse_set set{entt::deletion_policy::in_place};
+    entt::entity entities[3u];
+
+    entities[0u] = entt::entity{3};
+    entities[1u] = entt::entity{42};
+    entities[2u] = entt::entity{9};
+
+    ASSERT_TRUE(set.empty());
+    ASSERT_EQ(set.size(), 0u);
+
+    ASSERT_DEATH(set.erase(std::begin(entities), std::end(entities)), "");
+    ASSERT_DEATH(set.erase(entities[1u]), "");
+
+    ASSERT_TRUE(set.empty());
+    ASSERT_EQ(set.size(), 0u);
+
+    set.insert(std::begin(entities), std::end(entities));
+    set.erase(set.begin(), set.end());
+
+    ASSERT_FALSE(set.empty());
+    ASSERT_EQ(set.size(), 3u);
+    ASSERT_TRUE(set.at(0u) == entt::tombstone);
+    ASSERT_TRUE(set.at(1u) == entt::tombstone);
+    ASSERT_TRUE(set.at(2u) == entt::tombstone);
+
+    set.insert(std::begin(entities), std::end(entities));
+    set.erase(entities, entities + 2u);
+
+    ASSERT_FALSE(set.empty());
+    ASSERT_EQ(set.size(), 6u);
+    ASSERT_EQ(*set.begin(), entities[2u]);
+    ASSERT_TRUE(set.at(3u) == entt::tombstone);
+    ASSERT_TRUE(set.at(4u) == entt::tombstone);
+
+    set.erase(entities[2u]);
+
+    ASSERT_DEATH(set.erase(entities[2u]), "");
+    ASSERT_FALSE(set.empty());
+    ASSERT_EQ(set.size(), 6u);
+
+    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.size(), 9u);
+    ASSERT_TRUE(set.at(6u) == entt::tombstone);
+    ASSERT_EQ(set.at(7u), entities[2u]);
+    ASSERT_EQ(*++set.begin(), entities[2u]);
+    ASSERT_TRUE(set.at(8u) == entt::tombstone);
+
+    set.compact();
+
+    ASSERT_FALSE(set.empty());
+    ASSERT_EQ(set.size(), 1u);
+    ASSERT_EQ(*set.begin(), entities[2u]);
+
+    set.clear();
+
+    ASSERT_EQ(set.size(), 0u);
+
+    set.insert(std::begin(entities), std::end(entities));
+    set.erase(entities[2u]);
+
+    ASSERT_DEATH(set.erase(entities[2u]), "");
+
+    set.erase(entities[0u]);
+    set.erase(entities[1u]);
+
+    ASSERT_DEATH(set.erase(entities, entities + 2u), "");
+    ASSERT_EQ(set.size(), 3u);
+    ASSERT_TRUE(*set.begin() == entt::tombstone);
+
+    set.emplace(entities[0u]);
+
+    ASSERT_EQ(*++set.begin(), entities[0u]);
+
+    set.emplace(entities[1u]);
+    set.emplace(entities[2u]);
+    set.emplace(entt::entity{0});
+
+    ASSERT_EQ(set.size(), 4u);
+    ASSERT_EQ(*set.begin(), entt::entity{0});
+    ASSERT_EQ(set.at(0u), entities[1u]);
+    ASSERT_EQ(set.at(1u), entities[0u]);
+    ASSERT_EQ(set.at(2u), entities[2u]);
 }
 
 TEST(SparseSet, Remove) {
     entt::sparse_set set;
-    entt::entity entities[3];
+    entt::entity entities[3u];
 
-    entities[0] = entt::entity{3};
-    entities[1] = entt::entity{42};
-    entities[2] = entt::entity{9};
+    entities[0u] = entt::entity{3};
+    entities[1u] = entt::entity{42};
+    entities[2u] = entt::entity{9};
 
     ASSERT_TRUE(set.empty());
 
     ASSERT_EQ(set.remove(std::begin(entities), std::end(entities)), 0u);
-    ASSERT_EQ(set.remove(entities[1]), 0u);
+    ASSERT_EQ(set.remove(entities[1u]), 0u);
 
     ASSERT_TRUE(set.empty());
 
@@ -233,10 +366,10 @@ TEST(SparseSet, Remove) {
 
     ASSERT_EQ(set.remove(entities, entities + 2u), 2u);
     ASSERT_FALSE(set.empty());
-    ASSERT_EQ(*set.begin(), entt::entity{9});
+    ASSERT_EQ(*set.begin(), entities[2u]);
 
-    ASSERT_EQ(set.remove(entities[2]), 1u);
-    ASSERT_EQ(set.remove(entities[2]), 0u);
+    ASSERT_EQ(set.remove(entities[2u]), 1u);
+    ASSERT_EQ(set.remove(entities[2u]), 0u);
     ASSERT_TRUE(set.empty());
 
     set.insert(entities, entities + 2u);
@@ -245,11 +378,152 @@ TEST(SparseSet, Remove) {
     ASSERT_TRUE(set.empty());
 
     set.insert(std::begin(entities), std::end(entities));
-    std::swap(entities[1], entities[2]);
+    std::swap(entities[1u], entities[2u]);
 
     ASSERT_EQ(set.remove(entities, entities + 2u), 2u);
     ASSERT_FALSE(set.empty());
-    ASSERT_EQ(*set.begin(), entt::entity{42});
+    ASSERT_EQ(*set.begin(), entities[2u]);
+}
+
+TEST(SparseSet, StableRemove) {
+    entt::sparse_set set{entt::deletion_policy::in_place};
+    entt::entity entities[3u];
+
+    entities[0u] = entt::entity{3};
+    entities[1u] = entt::entity{42};
+    entities[2u] = entt::entity{9};
+
+    ASSERT_TRUE(set.empty());
+    ASSERT_EQ(set.size(), 0u);
+
+    ASSERT_EQ(set.remove(std::begin(entities), std::end(entities)), 0u);
+    ASSERT_EQ(set.remove(entities[1u]), 0u);
+
+    ASSERT_TRUE(set.empty());
+    ASSERT_EQ(set.size(), 0u);
+
+    set.insert(std::begin(entities), std::end(entities));
+
+    ASSERT_EQ(set.remove(set.begin(), set.end()), 3u);
+    ASSERT_FALSE(set.empty());
+    ASSERT_EQ(set.size(), 3u);
+    ASSERT_TRUE(set.at(0u) == entt::tombstone);
+    ASSERT_TRUE(set.at(1u) == entt::tombstone);
+    ASSERT_TRUE(set.at(2u) == 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.size(), 6u);
+    ASSERT_EQ(*set.begin(), entt::entity{9});
+    ASSERT_TRUE(set.at(3u) == entt::tombstone);
+    ASSERT_TRUE(set.at(4u) == entt::tombstone);
+
+    ASSERT_EQ(set.remove(entities[2u]), 1u);
+    ASSERT_EQ(set.remove(entities[2u]), 0u);
+    ASSERT_EQ(set.remove(entities[2u]), 0u);
+    ASSERT_EQ(set.remove(entities[2u]), 0u);
+    ASSERT_FALSE(set.empty());
+    ASSERT_EQ(set.size(), 6u);
+    ASSERT_TRUE(*set.begin() == entt::tombstone);
+
+    set.insert(entities, entities + 2u);
+
+    ASSERT_EQ(set.remove(std::begin(entities), std::end(entities)), 2u);
+    ASSERT_FALSE(set.empty());
+    ASSERT_EQ(set.size(), 8u);
+    ASSERT_TRUE(set.at(6u) == entt::tombstone);
+    ASSERT_TRUE(set.at(7u) == entt::tombstone);
+
+    set.insert(std::begin(entities), std::end(entities));
+    std::swap(entities[1u], entities[2u]);
+
+    ASSERT_EQ(set.remove(entities, entities + 2u), 2u);
+    ASSERT_FALSE(set.empty());
+    ASSERT_EQ(set.size(), 11u);
+    ASSERT_TRUE(set.at(8u) == entt::tombstone);
+    ASSERT_EQ(set.at(9u), entities[2u]);
+    ASSERT_EQ(*++set.begin(), entities[2u]);
+    ASSERT_TRUE(set.at(10u) == entt::tombstone);
+
+    set.compact();
+
+    ASSERT_FALSE(set.empty());
+    ASSERT_EQ(set.size(), 1u);
+    ASSERT_EQ(*set.begin(), entities[2u]);
+
+    set.clear();
+
+    ASSERT_EQ(set.size(), 0u);
+
+    set.insert(std::begin(entities), std::end(entities));
+
+    ASSERT_EQ(set.remove(entities[2u]), 1u);
+    ASSERT_EQ(set.remove(entities[2u]), 0u);
+
+    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_TRUE(*set.begin() == entt::tombstone);
+
+    set.emplace(entities[0u]);
+
+    ASSERT_EQ(*++set.begin(), entities[0u]);
+
+    set.emplace(entities[1u]);
+    set.emplace(entities[2u]);
+    set.emplace(entt::entity{0});
+
+    ASSERT_EQ(set.size(), 4u);
+    ASSERT_EQ(*set.begin(), entt::entity{0});
+    ASSERT_EQ(set.at(0u), entities[1u]);
+    ASSERT_EQ(set.at(1u), entities[0u]);
+    ASSERT_EQ(set.at(2u), entities[2u]);
+}
+
+TEST(SparseSet, Compact) {
+    entt::sparse_set set{entt::deletion_policy::in_place};
+
+    ASSERT_TRUE(set.empty());
+    ASSERT_EQ(set.size(), 0u);
+
+    set.compact();
+
+    ASSERT_TRUE(set.empty());
+    ASSERT_EQ(set.size(), 0u);
+
+    set.emplace(entt::entity{0});
+    set.compact();
+
+    ASSERT_FALSE(set.empty());
+    ASSERT_EQ(set.size(), 1u);
+
+    set.emplace(entt::entity{42});
+    set.erase(entt::entity{0});
+
+    ASSERT_EQ(set.size(), 2u);
+    ASSERT_EQ(set.index(entt::entity{42}), 1u);
+
+    set.compact();
+
+    ASSERT_EQ(set.size(), 1u);
+    ASSERT_EQ(set.index(entt::entity{42}), 0u);
+
+    set.emplace(entt::entity{0});
+    set.compact();
+
+    ASSERT_EQ(set.size(), 2u);
+    ASSERT_EQ(set.index(entt::entity{42}), 0u);
+    ASSERT_EQ(set.index(entt::entity{0}), 1u);
+
+    set.erase(entt::entity{0});
+    set.erase(entt::entity{42});
+    set.compact();
+
+    ASSERT_TRUE(set.empty());
 }
 
 TEST(SparseSet, Clear) {
@@ -299,7 +573,7 @@ TEST(SparseSet, Iterator) {
     ASSERT_EQ(end - (end - begin), set.begin());
     ASSERT_EQ(end + (begin - end), set.begin());
 
-    ASSERT_EQ(begin[0], *set.begin());
+    ASSERT_EQ(begin[0u], *set.begin());
 
     ASSERT_LT(begin, end);
     ASSERT_LE(begin, set.begin());
@@ -344,7 +618,7 @@ TEST(SparseSet, ReverseIterator) {
     ASSERT_EQ(end - (end - begin), set.rbegin());
     ASSERT_EQ(end + (begin - end), set.rbegin());
 
-    ASSERT_EQ(begin[0], *set.rbegin());
+    ASSERT_EQ(begin[0u], *set.rbegin());
 
     ASSERT_LT(begin, end);
     ASSERT_LE(begin, set.rbegin());
@@ -583,28 +857,6 @@ TEST(SparseSet, CanModifyDuringIteration) {
     (void)entity;
 }
 
-TEST(SparseSet, ThrowingEntity) {
-    entt::basic_sparse_set<test::throwing_entity> set{};
-    test::throwing_entity::trigger_on_entity = 0u;
-
-    // strong exception safety
-    ASSERT_THROW(set.emplace(42), typename test::throwing_entity::exception_type);
-    ASSERT_TRUE(set.empty());
-
-    test::throwing_entity::trigger_on_entity = 42u;
-    const test::throwing_entity entities[2u]{42, 1};
-
-    // basic exception safety
-    ASSERT_THROW(set.insert(std::begin(entities), std::end(entities)), typename test::throwing_entity::exception_type);
-    ASSERT_EQ(set.size(), 0u);
-    ASSERT_FALSE(set.contains(1));
-
-    // basic exception safety
-    ASSERT_THROW(set.insert(std::rbegin(entities), std::rend(entities)), typename test::throwing_entity::exception_type);
-    ASSERT_EQ(set.size(), 1u);
-    ASSERT_TRUE(set.contains(1));
-}
-
 TEST(SparseSet, ThrowingAllocator) {
     entt::basic_sparse_set<entt::entity, test::throwing_allocator<entt::entity>> set{};
 
@@ -615,16 +867,15 @@ TEST(SparseSet, ThrowingAllocator) {
     ASSERT_EQ(set.capacity(), 0u);
     ASSERT_EQ(set.extent(), 0u);
 
-    set.emplace(entt::entity{0});
     test::throwing_allocator<entt::entity>::trigger_on_allocate = true;
 
     // strong exception safety
-    ASSERT_THROW(set.reserve(2u), test::throwing_allocator<entt::entity>::exception_type);
-    ASSERT_EQ(set.capacity(), 1u);
-    ASSERT_EQ(set.extent(), ENTT_SPARSE_PAGE);
-    ASSERT_TRUE(set.contains(entt::entity{0}));
+    ASSERT_THROW(set.emplace(entt::entity{0}), test::throwing_allocator<entt::entity>::exception_type);
+    ASSERT_EQ(set.capacity(), 0u);
+    ASSERT_EQ(set.extent(), 0u);
 
-    test::throwing_allocator<entt::entity>::trigger_on_pointer_copy = true;
+    set.emplace(entt::entity{0});
+    test::throwing_allocator<entt::entity>::trigger_on_allocate = true;
 
     // strong exception safety
     ASSERT_THROW(set.reserve(2u), test::throwing_allocator<entt::entity>::exception_type);
@@ -632,28 +883,24 @@ TEST(SparseSet, ThrowingAllocator) {
     ASSERT_EQ(set.extent(), ENTT_SPARSE_PAGE);
     ASSERT_TRUE(set.contains(entt::entity{0}));
 
-    set.reserve(ENTT_PACKED_PAGE);
-    test::throwing_allocator<entt::entity>::trigger_on_pointer_copy = true;
+    entt::entity entities[2u]{entt::entity{1}, entt::entity{ENTT_SPARSE_PAGE}};
+    test::throwing_allocator<entt::entity>::trigger_after_allocate = true;
 
-    // strong exception safety
-    ASSERT_THROW(set.emplace(entt::entity{ENTT_SPARSE_PAGE + 1u}), test::throwing_allocator<entt::entity>::exception_type);
-    ASSERT_EQ(set.capacity(), ENTT_PACKED_PAGE);
-    ASSERT_EQ(set.extent(), ENTT_SPARSE_PAGE);
+    // basic exception safety
+    ASSERT_THROW(set.insert(std::begin(entities), std::end(entities)), test::throwing_allocator<entt::entity>::exception_type);
+    ASSERT_EQ(set.capacity(), 3u);
+    ASSERT_EQ(set.size(), 2u);
+    ASSERT_EQ(set.extent(), 2 * ENTT_SPARSE_PAGE);
     ASSERT_TRUE(set.contains(entt::entity{0}));
+    ASSERT_TRUE(set.contains(entt::entity{1}));
+    ASSERT_FALSE(set.contains(entt::entity{ENTT_SPARSE_PAGE}));
+
+    set.emplace(entities[1u]);
+
+    ASSERT_TRUE(set.contains(entt::entity{ENTT_SPARSE_PAGE}));
 
     // unnecessary but they test a bit of template machinery :)
     set.clear();
     set.shrink_to_fit();
     set = decltype(set){};
-
-    set.reserve(ENTT_PACKED_PAGE);
-    set.emplace(entt::entity{ENTT_SPARSE_PAGE + 1u});
-    test::throwing_allocator<entt::entity>::trigger_on_pointer_copy = true;
-
-    // strong exception safety
-    ASSERT_THROW(set.emplace(entt::entity{0}), test::throwing_allocator<entt::entity>::exception_type);
-    ASSERT_EQ(set.capacity(), ENTT_PACKED_PAGE);
-    ASSERT_EQ(set.extent(), 2 * ENTT_SPARSE_PAGE);
-    ASSERT_FALSE(set.contains(entt::entity{0}));
-    ASSERT_TRUE(set.contains(entt::entity{ENTT_SPARSE_PAGE + 1u}));
 }

+ 355 - 103
test/entt/entity/storage.cpp

@@ -5,14 +5,21 @@
 #include <type_traits>
 #include <unordered_set>
 #include <gtest/gtest.h>
-#include <entt/entity/storage.hpp>
+#include <entt/entity/component.hpp>
 #include <entt/entity/fwd.hpp>
+#include <entt/entity/storage.hpp>
 #include "throwing_allocator.hpp"
 #include "throwing_component.hpp"
-#include "throwing_entity.hpp"
 
 struct empty_type {};
 struct boxed_int { int value; };
+struct stable_type { int value; };
+
+template<>
+struct entt::component_traits<stable_type> {
+    using in_place_delete = std::true_type;
+    using ignore_if_empty = std::true_type;
+};
 
 bool operator==(const boxed_int &lhs, const boxed_int &rhs) {
     return lhs.value == rhs.value;
@@ -129,32 +136,32 @@ TEST(Storage, EmptyType) {
 
 TEST(Storage, Insert) {
     entt::storage<int> pool;
-    entt::entity entities[2];
+    entt::entity entities[2u];
 
-    entities[0] = entt::entity{3};
-    entities[1] = entt::entity{42};
+    entities[0u] = entt::entity{3};
+    entities[1u] = entt::entity{42};
     pool.insert(std::begin(entities), std::end(entities), {});
 
-    ASSERT_TRUE(pool.contains(entities[0]));
-    ASSERT_TRUE(pool.contains(entities[1]));
+    ASSERT_TRUE(pool.contains(entities[0u]));
+    ASSERT_TRUE(pool.contains(entities[1u]));
 
     ASSERT_FALSE(pool.empty());
     ASSERT_EQ(pool.size(), 2u);
-    ASSERT_EQ(pool.get(entities[0]), 0);
-    ASSERT_EQ(pool.get(entities[1]), 0);
+    ASSERT_EQ(pool.get(entities[0u]), 0);
+    ASSERT_EQ(pool.get(entities[1u]), 0);
 }
 
 TEST(Storage, InsertEmptyType) {
     entt::storage<empty_type> pool;
-    entt::entity entities[2];
+    entt::entity entities[2u];
 
-    entities[0] = entt::entity{3};
-    entities[1] = entt::entity{42};
+    entities[0u] = entt::entity{3};
+    entities[1u] = entt::entity{42};
 
     pool.insert(std::begin(entities), std::end(entities));
 
-    ASSERT_TRUE(pool.contains(entities[0]));
-    ASSERT_TRUE(pool.contains(entities[1]));
+    ASSERT_TRUE(pool.contains(entities[0u]));
+    ASSERT_TRUE(pool.contains(entities[1u]));
 
     ASSERT_FALSE(pool.empty());
     ASSERT_EQ(pool.size(), 2u);
@@ -162,81 +169,340 @@ TEST(Storage, InsertEmptyType) {
 
 TEST(Storage, Erase) {
     entt::storage<int> pool;
-    entt::entity entities[3];
+    entt::entity entities[3u];
 
-    entities[0] = entt::entity{3};
-    entities[1] = entt::entity{42};
-    entities[2] = entt::entity{9};
+    entities[0u] = entt::entity{3};
+    entities[1u] = entt::entity{42};
+    entities[2u] = entt::entity{9};
 
-    pool.emplace(entities[0]);
-    pool.emplace(entities[1]);
-    pool.emplace(entities[2]);
+    pool.emplace(entities[0u]);
+    pool.emplace(entities[1u]);
+    pool.emplace(entities[2u]);
     pool.erase(std::begin(entities), std::end(entities));
 
     ASSERT_DEATH(pool.erase(std::begin(entities), std::end(entities)), "");
     ASSERT_TRUE(pool.empty());
 
-    pool.emplace(entities[0], 0);
-    pool.emplace(entities[1], 1);
-    pool.emplace(entities[2], 2);
+    pool.emplace(entities[0u], 0);
+    pool.emplace(entities[1u], 1);
+    pool.emplace(entities[2u], 2);
     pool.erase(entities, entities + 2u);
 
     ASSERT_FALSE(pool.empty());
     ASSERT_EQ(*pool.begin(), 2);
 
-    pool.erase(entities[2]);
+    pool.erase(entities[2u]);
 
-    ASSERT_DEATH(pool.erase(entities[2]), "");
+    ASSERT_DEATH(pool.erase(entities[2u]), "");
     ASSERT_TRUE(pool.empty());
 
-    pool.emplace(entities[0], 0);
-    pool.emplace(entities[1], 1);
-    pool.emplace(entities[2], 2);
-    std::swap(entities[1], entities[2]);
+    pool.emplace(entities[0u], 0);
+    pool.emplace(entities[1u], 1);
+    pool.emplace(entities[2u], 2);
+    std::swap(entities[1u], entities[2u]);
     pool.erase(entities, entities + 2u);
 
     ASSERT_FALSE(pool.empty());
     ASSERT_EQ(*pool.begin(), 1);
 }
 
+TEST(Storage, StableErase) {
+    entt::storage<stable_type> pool;
+    entt::entity entities[3u];
+
+    ASSERT_DEATH([[maybe_unused]] auto &&value = pool.get(entt::tombstone), "");
+    ASSERT_DEATH([[maybe_unused]] auto &&value = pool.get(entt::null), "");
+
+    entities[0u] = entt::entity{3};
+    entities[1u] = entt::entity{42};
+    entities[2u] = entt::entity{9};
+
+    pool.emplace(entities[0u], stable_type{0});
+    pool.emplace(entities[1u], stable_type{1});
+    pool.emplace(entities[2u], stable_type{2});
+
+    pool.erase(std::begin(entities), std::end(entities));
+
+    ASSERT_DEATH(pool.erase(std::begin(entities), std::end(entities)), "");
+    ASSERT_FALSE(pool.empty());
+    ASSERT_EQ(pool.size(), 3u);
+    ASSERT_TRUE(pool.at(2u) == entt::tombstone);
+    ASSERT_DEATH([[maybe_unused]] auto &&value = pool.get(entt::tombstone), "");
+    ASSERT_DEATH([[maybe_unused]] auto &&value = pool.get(entt::null), "");
+    ASSERT_DEATH([[maybe_unused]] auto &&value = pool.get(entities[1u]), "");
+
+    pool.emplace(entities[2u], stable_type{2});
+    pool.emplace(entities[0u], stable_type{0});
+    pool.emplace(entities[1u], stable_type{1});
+
+    ASSERT_EQ(pool.get(entities[0u]).value, 0);
+    ASSERT_EQ(pool.get(entities[1u]).value, 1);
+    ASSERT_EQ(pool.get(entities[2u]).value, 2);
+
+    ASSERT_EQ(pool.begin()->value, 2);
+    ASSERT_EQ(pool.index(entities[0u]), 1u);
+    ASSERT_EQ(pool.index(entities[1u]), 0u);
+    ASSERT_EQ(pool.index(entities[2u]), 2u);
+
+    pool.erase(entities, entities + 2u);
+
+    ASSERT_FALSE(pool.empty());
+    ASSERT_EQ(pool.size(), 3u);
+    ASSERT_EQ(pool.begin()->value, 2);
+    ASSERT_EQ(pool.index(entities[2u]), 2u);
+
+    pool.erase(entities[2u]);
+
+    ASSERT_DEATH(pool.erase(entities[2u]), "");
+    ASSERT_FALSE(pool.empty());
+    ASSERT_EQ(pool.size(), 3u);
+    ASSERT_FALSE(pool.contains(entities[0u]));
+    ASSERT_FALSE(pool.contains(entities[1u]));
+    ASSERT_FALSE(pool.contains(entities[2u]));
+
+    pool.emplace(entities[0u], stable_type{0});
+    pool.emplace(entities[1u], stable_type{1});
+    pool.emplace(entities[2u], stable_type{2});
+    std::swap(entities[1u], entities[2u]);
+    pool.erase(entities, entities + 2u);
+
+    ASSERT_FALSE(pool.empty());
+    ASSERT_EQ(pool.size(), 3u);
+    ASSERT_TRUE(pool.contains(entities[2u]));
+    ASSERT_EQ(pool.index(entities[2u]), 0u);
+    ASSERT_EQ(pool.get(entities[2u]).value, 1);
+
+    pool.compact();
+
+    ASSERT_FALSE(pool.empty());
+    ASSERT_EQ(pool.size(), 1u);
+    ASSERT_EQ(pool.begin()->value, 1);
+
+    pool.clear();
+
+    ASSERT_EQ(pool.size(), 0u);
+
+    pool.emplace(entities[0u], stable_type{0});
+    pool.emplace(entities[1u], stable_type{2});
+    pool.emplace(entities[2u], stable_type{1});
+    pool.erase(entities[2u]);
+
+    ASSERT_DEATH(pool.erase(entities[2u]), "");
+
+    pool.erase(entities[0u]);
+    pool.erase(entities[1u]);
+
+    ASSERT_DEATH(pool.erase(entities, entities + 2u), "");
+    ASSERT_EQ(pool.size(), 3u);
+    ASSERT_TRUE(pool.at(2u) == entt::tombstone);
+
+    pool.emplace(entities[0u], stable_type{99});
+
+    ASSERT_EQ((++pool.begin())->value, 99);
+
+    pool.emplace(entities[1u], stable_type{2});
+    pool.emplace(entities[2u], stable_type{1});
+    pool.emplace(entt::entity{0}, stable_type{7});
+
+    ASSERT_EQ(pool.size(), 4u);
+    ASSERT_EQ(pool.begin()->value, 7);
+    ASSERT_EQ(pool.at(0u), entities[1u]);
+    ASSERT_EQ(pool.at(1u), entities[0u]);
+    ASSERT_EQ(pool.at(2u), entities[2u]);
+
+    ASSERT_EQ(pool.get(entities[0u]).value, 99);
+    ASSERT_EQ(pool.get(entities[1u]).value, 2);
+    ASSERT_EQ(pool.get(entities[2u]).value, 1);
+}
+
 TEST(Storage, Remove) {
     entt::storage<int> pool;
-    entt::entity entities[3];
+    entt::entity entities[3u];
 
-    entities[0] = entt::entity{3};
-    entities[1] = entt::entity{42};
-    entities[2] = entt::entity{9};
+    entities[0u] = entt::entity{3};
+    entities[1u] = entt::entity{42};
+    entities[2u] = entt::entity{9};
 
-    pool.emplace(entities[0]);
-    pool.emplace(entities[1]);
-    pool.emplace(entities[2]);
+    pool.emplace(entities[0u]);
+    pool.emplace(entities[1u]);
+    pool.emplace(entities[2u]);
 
     ASSERT_EQ(pool.remove(std::begin(entities), std::end(entities)), 3u);
     ASSERT_EQ(pool.remove(std::begin(entities), std::end(entities)), 0u);
     ASSERT_TRUE(pool.empty());
 
-    pool.emplace(entities[0], 0);
-    pool.emplace(entities[1], 1);
-    pool.emplace(entities[2], 2);
+    pool.emplace(entities[0u], 0);
+    pool.emplace(entities[1u], 1);
+    pool.emplace(entities[2u], 2);
 
     ASSERT_EQ(pool.remove(entities, entities + 2u), 2u);
     ASSERT_FALSE(pool.empty());
     ASSERT_EQ(*pool.begin(), 2);
 
-    ASSERT_EQ(pool.remove(entities[2]), 1u);
-    ASSERT_EQ(pool.remove(entities[2]), 0u);
+    ASSERT_EQ(pool.remove(entities[2u]), 1u);
+    ASSERT_EQ(pool.remove(entities[2u]), 0u);
     ASSERT_TRUE(pool.empty());
 
-    pool.emplace(entities[0], 0);
-    pool.emplace(entities[1], 1);
-    pool.emplace(entities[2], 2);
-    std::swap(entities[1], entities[2]);
+    pool.emplace(entities[0u], 0);
+    pool.emplace(entities[1u], 1);
+    pool.emplace(entities[2u], 2);
+    std::swap(entities[1u], entities[2u]);
 
     ASSERT_EQ(pool.remove(entities, entities + 2u), 2u);
     ASSERT_FALSE(pool.empty());
     ASSERT_EQ(*pool.begin(), 1);
 }
 
+TEST(Storage, StableRemove) {
+    entt::storage<stable_type> pool;
+    entt::entity entities[3u];
+
+    entities[0u] = entt::entity{3};
+    entities[1u] = entt::entity{42};
+    entities[2u] = entt::entity{9};
+
+    pool.emplace(entities[0u], stable_type{0});
+    pool.emplace(entities[1u], stable_type{1});
+    pool.emplace(entities[2u], stable_type{2});
+
+    ASSERT_EQ(pool.remove(std::begin(entities), std::end(entities)), 3u);
+    ASSERT_EQ(pool.remove(std::begin(entities), std::end(entities)), 0u);
+    ASSERT_FALSE(pool.empty());
+    ASSERT_EQ(pool.size(), 3u);
+    ASSERT_TRUE(pool.at(2u) == entt::tombstone);
+    ASSERT_DEATH([[maybe_unused]] auto &&value = pool.get(entt::tombstone), "");
+    ASSERT_DEATH([[maybe_unused]] auto &&value = pool.get(entt::null), "");
+    ASSERT_DEATH([[maybe_unused]] auto &&value = pool.get(entities[1u]), "");
+
+    pool.emplace(entities[2u], stable_type{2});
+    pool.emplace(entities[0u], stable_type{0});
+    pool.emplace(entities[1u], stable_type{1});
+
+    ASSERT_EQ(pool.get(entities[0u]).value, 0);
+    ASSERT_EQ(pool.get(entities[1u]).value, 1);
+    ASSERT_EQ(pool.get(entities[2u]).value, 2);
+
+    ASSERT_EQ(pool.begin()->value, 2);
+    ASSERT_EQ(pool.index(entities[0u]), 1u);
+    ASSERT_EQ(pool.index(entities[1u]), 0u);
+    ASSERT_EQ(pool.index(entities[2u]), 2u);
+
+    ASSERT_EQ(pool.remove(entities, entities + 2u), 2u);
+    ASSERT_FALSE(pool.empty());
+    ASSERT_EQ(pool.size(), 3u);
+    ASSERT_EQ(pool.begin()->value, 2);
+    ASSERT_EQ(pool.index(entities[2u]), 2u);
+
+    ASSERT_EQ(pool.remove(entities[2u]), 1u);
+    ASSERT_EQ(pool.remove(entities[2u]), 0u);
+    ASSERT_EQ(pool.remove(entities[2u]), 0u);
+    ASSERT_FALSE(pool.empty());
+    ASSERT_EQ(pool.size(), 3u);
+    ASSERT_FALSE(pool.contains(entities[0u]));
+    ASSERT_FALSE(pool.contains(entities[1u]));
+    ASSERT_FALSE(pool.contains(entities[2u]));
+
+    pool.emplace(entities[0u], stable_type{0});
+    pool.emplace(entities[1u], stable_type{1});
+    pool.emplace(entities[2u], stable_type{2});
+    std::swap(entities[1u], entities[2u]);
+
+    ASSERT_EQ(pool.remove(entities, entities + 2u), 2u);
+    ASSERT_FALSE(pool.empty());
+    ASSERT_EQ(pool.size(), 3u);
+    ASSERT_TRUE(pool.contains(entities[2u]));
+    ASSERT_EQ(pool.index(entities[2u]), 0u);
+    ASSERT_EQ(pool.get(entities[2u]).value, 1);
+
+    pool.compact();
+
+    ASSERT_FALSE(pool.empty());
+    ASSERT_EQ(pool.size(), 1u);
+    ASSERT_EQ(pool.begin()->value, 1);
+
+    pool.clear();
+
+    ASSERT_EQ(pool.size(), 0u);
+
+    pool.emplace(entities[0u], stable_type{0});
+    pool.emplace(entities[1u], stable_type{2});
+    pool.emplace(entities[2u], stable_type{1});
+
+    ASSERT_EQ(pool.remove(entities[2u]), 1u);
+    ASSERT_EQ(pool.remove(entities[2u]), 0u);
+
+    ASSERT_EQ(pool.remove(entities[0u]), 1u);
+    ASSERT_EQ(pool.remove(entities[1u]), 1u);
+    ASSERT_EQ(pool.remove(entities, entities + 2u), 0u);
+
+    ASSERT_EQ(pool.size(), 3u);
+    ASSERT_TRUE(pool.at(2u) == entt::tombstone);
+
+    pool.emplace(entities[0u], stable_type{99});
+
+    ASSERT_EQ((++pool.begin())->value, 99);
+
+    pool.emplace(entities[1u], stable_type{2});
+    pool.emplace(entities[2u], stable_type{1});
+    pool.emplace(entt::entity{0}, stable_type{7});
+
+    ASSERT_EQ(pool.size(), 4u);
+    ASSERT_EQ(pool.begin()->value, 7);
+    ASSERT_EQ(pool.at(0u), entities[1u]);
+    ASSERT_EQ(pool.at(1u), entities[0u]);
+    ASSERT_EQ(pool.at(2u), entities[2u]);
+
+    ASSERT_EQ(pool.get(entities[0u]).value, 99);
+    ASSERT_EQ(pool.get(entities[1u]).value, 2);
+    ASSERT_EQ(pool.get(entities[2u]).value, 1);
+}
+
+TEST(Storage, Compact) {
+    entt::storage<stable_type> pool;
+
+    ASSERT_TRUE(pool.empty());
+    ASSERT_EQ(pool.size(), 0u);
+
+    pool.compact();
+
+    ASSERT_TRUE(pool.empty());
+    ASSERT_EQ(pool.size(), 0u);
+
+    pool.emplace(entt::entity{0}, stable_type{0});
+    pool.compact();
+
+    ASSERT_FALSE(pool.empty());
+    ASSERT_EQ(pool.size(), 1u);
+
+    pool.emplace(entt::entity{42}, stable_type{42});
+    pool.erase(entt::entity{0});
+
+    ASSERT_EQ(pool.size(), 2u);
+    ASSERT_EQ(pool.index(entt::entity{42}), 1u);
+    ASSERT_EQ(pool.get(entt::entity{42}).value, 42);
+
+    pool.compact();
+
+    ASSERT_EQ(pool.size(), 1u);
+    ASSERT_EQ(pool.index(entt::entity{42}), 0u);
+    ASSERT_EQ(pool.get(entt::entity{42}).value, 42);
+
+    pool.emplace(entt::entity{0}, stable_type{0});
+    pool.compact();
+
+    ASSERT_EQ(pool.size(), 2u);
+    ASSERT_EQ(pool.index(entt::entity{42}), 0u);
+    ASSERT_EQ(pool.index(entt::entity{0}), 1u);
+    ASSERT_EQ(pool.get(entt::entity{42}).value, 42);
+    ASSERT_EQ(pool.get(entt::entity{0}).value, 0);
+
+    pool.erase(entt::entity{0});
+    pool.erase(entt::entity{42});
+    pool.compact();
+
+    ASSERT_TRUE(pool.empty());
+}
+
 TEST(Storage, ShrinkToFit) {
     entt::storage<int> pool;
 
@@ -312,7 +578,7 @@ TEST(Storage, Iterator) {
     ASSERT_EQ(end - (end - begin), pool.begin());
     ASSERT_EQ(end + (begin - end), pool.begin());
 
-    ASSERT_EQ(begin[0].value, pool.begin()->value);
+    ASSERT_EQ(begin[0u].value, pool.begin()->value);
 
     ASSERT_LT(begin, end);
     ASSERT_LE(begin, pool.begin());
@@ -354,7 +620,7 @@ TEST(Storage, ConstIterator) {
     ASSERT_EQ(cend - (cend - cbegin), pool.cbegin());
     ASSERT_EQ(cend + (cbegin - cend), pool.cbegin());
 
-    ASSERT_EQ(cbegin[0].value, pool.cbegin()->value);
+    ASSERT_EQ(cbegin[0u].value, pool.cbegin()->value);
 
     ASSERT_LT(cbegin, cend);
     ASSERT_LE(cbegin, pool.cbegin());
@@ -396,7 +662,7 @@ TEST(Storage, ReverseIterator) {
     ASSERT_EQ(end - (end - begin), pool.rbegin());
     ASSERT_EQ(end + (begin - end), pool.rbegin());
 
-    ASSERT_EQ(begin[0].value, pool.rbegin()->value);
+    ASSERT_EQ(begin[0u].value, pool.rbegin()->value);
 
     ASSERT_LT(begin, end);
     ASSERT_LE(begin, pool.rbegin());
@@ -438,7 +704,7 @@ TEST(Storage, ConstReverseIterator) {
     ASSERT_EQ(cend - (cend - cbegin), pool.crbegin());
     ASSERT_EQ(cend + (cbegin - cend), pool.crbegin());
 
-    ASSERT_EQ(cbegin[0].value, pool.crbegin()->value);
+    ASSERT_EQ(cbegin[0u].value, pool.crbegin()->value);
 
     ASSERT_LT(cbegin, cend);
     ASSERT_LE(cbegin, pool.crbegin());
@@ -779,42 +1045,6 @@ TEST(Storage, UpdateFromDestructor) {
     test(entt::entity{0u});
 }
 
-TEST(Storage, ThrowingEntity) {
-    entt::basic_storage<test::throwing_entity, int> pool;
-    test::throwing_entity::trigger_on_entity = 42u;
-
-    // strong exception safety
-    ASSERT_THROW(pool.emplace(42, 0), typename test::throwing_entity::exception_type);
-    ASSERT_TRUE(pool.empty());
-
-    const test::throwing_entity entities[2u]{42, 1};
-    const int components[2u]{42, 1};
-
-    // basic exception safety
-    ASSERT_THROW(pool.insert(std::begin(entities), std::end(entities), 1), typename test::throwing_entity::exception_type);
-    ASSERT_EQ(pool.size(), 0u);
-    ASSERT_FALSE(pool.contains(1));
-
-    // basic exception safety
-    ASSERT_THROW(pool.insert(std::rbegin(entities), std::rend(entities), 1), typename test::throwing_entity::exception_type);
-    ASSERT_EQ(pool.size(), 1u);
-    ASSERT_TRUE(pool.contains(1));
-    ASSERT_EQ(pool.get(1), 1);
-
-    pool.clear();
-
-    // basic exception safety
-    ASSERT_THROW(pool.insert(std::begin(entities), std::end(entities), std::begin(components)), typename test::throwing_entity::exception_type);
-    ASSERT_EQ(pool.size(), 0u);
-    ASSERT_FALSE(pool.contains(1));
-
-    // basic exception safety
-    ASSERT_THROW(pool.insert(std::rbegin(entities), std::rend(entities), std::rbegin(components)), typename test::throwing_entity::exception_type);
-    ASSERT_EQ(pool.size(), 1u);
-    ASSERT_TRUE(pool.contains(1));
-    ASSERT_EQ(pool.get(1), 1);
-}
-
 TEST(Storage, ThrowingComponent) {
     entt::storage<test::throwing_component> pool;
     test::throwing_component::trigger_on_value = 42;
@@ -846,15 +1076,25 @@ TEST(Storage, ThrowingComponent) {
     pool.emplace(entt::entity{1}, 1);
     pool.emplace(entt::entity{42}, 42);
 
+    // basic exception safety
     ASSERT_THROW(pool.erase(entt::entity{1}), typename test::throwing_component::exception_type);
-    ASSERT_FALSE(pool.empty());
+    ASSERT_EQ(pool.size(), 2u);
+    ASSERT_TRUE(pool.contains(entt::entity{42}));
+    ASSERT_TRUE(pool.contains(entt::entity{1}));
+    ASSERT_EQ(pool.at(0u), entt::entity{1});
+    ASSERT_EQ(pool.at(1u), entt::entity{42});
+    ASSERT_EQ(pool.get(entt::entity{42}), 42);
+    // the element may have been moved but it's still there
+    ASSERT_EQ(pool.get(entt::entity{1}), test::throwing_component::moved_from_value);
+
+    test::throwing_component::trigger_on_value = 99;
+    pool.erase(entt::entity{1});
+
     ASSERT_EQ(pool.size(), 1u);
     ASSERT_TRUE(pool.contains(entt::entity{42}));
     ASSERT_FALSE(pool.contains(entt::entity{1}));
     ASSERT_EQ(pool.at(0u), entt::entity{42});
-    ASSERT_EQ(pool.at(1u), static_cast<entt::entity>(entt::null));
-    // basice exception safety: no-leak guarantee, stored data contain valid values which may differ from the original values
-    ASSERT_EQ(pool.get(entt::entity{42}), 1);
+    ASSERT_EQ(pool.get(entt::entity{42}), 42);
 }
 
 TEST(Storage, ThrowingAllocator) {
@@ -872,19 +1112,6 @@ TEST(Storage, ThrowingAllocator) {
     ASSERT_THROW(pool.reserve(2 * ENTT_PACKED_PAGE), test::throwing_allocator<int>::exception_type);
     ASSERT_EQ(pool.capacity(), 0u);
 
-    test::throwing_allocator<int>::trigger_on_pointer_copy = true;
-
-    // strong exception safety
-    ASSERT_THROW(pool.reserve(1u), test::throwing_allocator<int>::exception_type);
-    ASSERT_EQ(pool.capacity(), 0u);
-
-    pool.reserve(2 * ENTT_PACKED_PAGE);
-    test::throwing_allocator<test::throwing_allocator<int>::pointer>::trigger_on_pointer_copy = true;
-
-    // strong exception safety
-    ASSERT_THROW(pool.shrink_to_fit(), test::throwing_allocator<test::throwing_allocator<int>::pointer>::exception_type);
-    ASSERT_EQ(pool.capacity(), 2 * ENTT_PACKED_PAGE);
-
     pool.shrink_to_fit();
     test::throwing_allocator<int>::trigger_on_allocate = true;
 
@@ -892,4 +1119,29 @@ TEST(Storage, ThrowingAllocator) {
     ASSERT_THROW(pool.emplace(entt::entity{0}, 0), test::throwing_allocator<int>::exception_type);
     ASSERT_FALSE(pool.contains(entt::entity{0}));
     ASSERT_TRUE(pool.empty());
+
+    test::throwing_allocator<entt::entity>::trigger_on_allocate = true;
+
+    // strong exception safety
+    ASSERT_THROW(pool.emplace(entt::entity{0}, 0), test::throwing_allocator<entt::entity>::exception_type);
+    ASSERT_FALSE(pool.contains(entt::entity{0}));
+    ASSERT_TRUE(pool.empty());
+
+    pool.emplace(entt::entity{0}, 0);
+    const entt::entity entities[2u]{entt::entity{1}, entt::entity{ENTT_SPARSE_PAGE}};
+    test::throwing_allocator<entt::entity>::trigger_after_allocate = true;
+
+    // basic exception safety
+    ASSERT_THROW(pool.insert(std::begin(entities), std::end(entities), 0), test::throwing_allocator<entt::entity>::exception_type);
+    ASSERT_TRUE(pool.contains(entt::entity{1}));
+    ASSERT_FALSE(pool.contains(entt::entity{ENTT_SPARSE_PAGE}));
+
+    pool.erase(entt::entity{1});
+    const int components[2u]{1, ENTT_SPARSE_PAGE};
+    test::throwing_allocator<entt::entity>::trigger_on_allocate = true;
+
+    // basic exception safety
+    ASSERT_THROW(pool.insert(std::begin(entities), std::end(entities), std::begin(components)), test::throwing_allocator<entt::entity>::exception_type);
+    ASSERT_TRUE(pool.contains(entt::entity{1}));
+    ASSERT_FALSE(pool.contains(entt::entity{ENTT_SPARSE_PAGE}));
 }

+ 6 - 113
test/entt/entity/throwing_allocator.hpp

@@ -3,7 +3,6 @@
 
 
 #include <cstddef>
-#include <iterator>
 #include <memory>
 #include <type_traits>
 
@@ -16,123 +15,18 @@ class throwing_allocator {
     template<typename Other>
     friend class throwing_allocator;
 
-    struct fancy_pointer final {
-        using difference_type = typename std::iterator_traits<Type *>::difference_type;
-        using element_type = Type;
-        using value_type = element_type;
-        using pointer = value_type *;
-        using reference = value_type &;
-        using iterator_category = std::random_access_iterator_tag;
-
-        fancy_pointer(Type *init = nullptr)
-            : ptr{init}
-        {}
-
-        fancy_pointer(const fancy_pointer &other)
-            : ptr{other.ptr}
-        {
-            if(throwing_allocator::trigger_on_pointer_copy) {
-                throwing_allocator::trigger_on_pointer_copy = false;
-                throw test_exception{};
-            }
-        }
-
-        fancy_pointer & operator++() {
-            return ++ptr, *this;
-        }
-
-        fancy_pointer operator++(int) {
-            auto orig = *this;
-            return ++(*this), orig;
-        }
-
-        fancy_pointer & operator--() {
-            return --ptr, *this;
-        }
-
-        fancy_pointer operator--(int) {
-            auto orig = *this;
-            return operator--(), orig;
-        }
-
-        fancy_pointer & operator+=(const difference_type value) {
-            return (ptr += value, *this);
-        }
-
-        fancy_pointer operator+(const difference_type value) const {
-            auto copy = *this;
-            return (copy += value);
-        }
-
-        fancy_pointer & operator-=(const difference_type value) {
-            return (ptr -= value, *this);
-        }
-
-        fancy_pointer operator-(const difference_type value) const {
-            auto copy = *this;
-            return (copy -= value);
-        }
-
-        difference_type operator-(const fancy_pointer &other) const {
-            return ptr - other.ptr;
-        }
-
-        [[nodiscard]] reference operator[](const difference_type value) const {
-            return ptr[value];
-        }
-
-        [[nodiscard]] bool operator==(const fancy_pointer &other) const {
-            return other.ptr == ptr;
-        }
-
-        [[nodiscard]] bool operator!=(const fancy_pointer &other) const {
-            return !(*this == other);
-        }
-
-        [[nodiscard]] bool operator<(const fancy_pointer &other) const {
-            return ptr > other.ptr;
-        }
-
-        [[nodiscard]] bool operator>(const fancy_pointer &other) const {
-            return ptr < other.ptr;
-        }
-
-        [[nodiscard]] bool operator<=(const fancy_pointer &other) const {
-            return !(*this > other);
-        }
-
-        [[nodiscard]] bool operator>=(const fancy_pointer &other) const {
-            return !(*this < other);
-        }
-
-        explicit operator bool() const {
-            return (ptr != nullptr);
-        }
-
-        [[nodiscard]] pointer operator->() const {
-            return ptr;
-        }
-
-        [[nodiscard]] reference operator*() const {
-            return *ptr;
-        }
-
-    private:
-        Type *ptr;
-    };
-
     struct test_exception {};
 
 public:
     using value_type = Type;
-    using pointer = fancy_pointer;
-    using const_pointer = fancy_pointer;
-    using void_pointer = fancy_pointer;
-    using const_void_pointer = fancy_pointer;
+    using pointer = value_type *;
+    using const_pointer = const value_type *;
+    using void_pointer = void *;
+    using const_void_pointer = const void *;
     using propagate_on_container_move_assignment = std::true_type;
     using exception_type = test_exception;
 
-    constexpr throwing_allocator() = default;
+    throwing_allocator() = default;
 
     template<class Other>
     throwing_allocator(const throwing_allocator<Other> &other)
@@ -152,12 +46,11 @@ public:
     }
 
     void deallocate(pointer mem, std::size_t length) {
-        allocator.deallocate(mem.operator->(), length);
+        allocator.deallocate(mem, length);
     }
 
     static inline bool trigger_on_allocate{};
     static inline bool trigger_after_allocate{};
-    static inline bool trigger_on_pointer_copy{};
 
 private:
     std::allocator<Type> allocator;

+ 3 - 0
test/entt/entity/throwing_component.hpp

@@ -10,6 +10,7 @@ class throwing_component {
 
 public:
     using exception_type = test_exception;
+    static constexpr auto moved_from_value = -1;
 
     throwing_component(int value)
         : data{value}
@@ -19,12 +20,14 @@ public:
         : data{other.data}
     {
         if(data == trigger_on_value) {
+            data = moved_from_value;
             throw exception_type{};
         }
     }
 
     throwing_component & operator=(const throwing_component &other) {
         if(other.data == trigger_on_value) {
+            data = moved_from_value;
             throw exception_type{};
         }
 

+ 0 - 52
test/entt/entity/throwing_entity.hpp

@@ -1,52 +0,0 @@
-#ifndef ENTT_ENTITY_THROWING_ENTITY_HPP
-#define ENTT_ENTITY_THROWING_ENTITY_HPP
-
-
-namespace test {
-
-
-class throwing_entity {
-    struct test_exception {};
-
-public:
-    using entity_type = std::uint32_t;
-    using exception_type = test_exception;
-
-    static constexpr entity_type null = entt::null;
-
-    throwing_entity(entity_type value)
-        : entt{value}
-    {}
-
-    throwing_entity(const throwing_entity &other)
-        : entt{other.entt}
-    {
-        if(entt == trigger_on_entity) {
-            throw exception_type{};
-        }
-    }
-
-    throwing_entity & operator=(const throwing_entity &other) {
-        if(other.entt == trigger_on_entity) {
-            throw exception_type{};
-        }
-
-        entt = other.entt;
-        return *this;
-    }
-
-    operator entity_type() const {
-        return entt;
-    }
-
-    static inline entity_type trigger_on_entity{null};
-
-private:
-    entity_type entt{};
-};
-
-
-}
-
-
-#endif