Selaa lähdekoodia

sparse_set:
* (almost) allocator-aware container
* unequal containers not supported
* copying a sparse set isn't supported

Michele Caini 4 vuotta sitten
vanhempi
commit
c41509151b

+ 2 - 2
TODO

@@ -10,11 +10,11 @@ WIP:
 * 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
-* general purpose paged vector container
+* dedicated entity storage, in-place O(1) release/destroy for non-orphaned entities
 * custom allocators all over
 
 WIP:
+* general purpose paged vector container
 * 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.

+ 120 - 82
src/entt/entity/sparse_set.hpp

@@ -68,8 +68,7 @@ class basic_sparse_set {
 
     using entity_traits = entt_traits<Entity>;
 
-    static_assert(alloc_traits::propagate_on_container_move_assignment::value);
-    static_assert(alloc_ptr_traits::propagate_on_container_move_assignment::value);
+    static_assert(alloc_traits::is_always_equal::value, "Unequal allocators not supported");
 
     struct sparse_set_iterator final {
         using difference_type = typename entity_traits::difference_type;
@@ -81,7 +80,7 @@ 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},
+            : packed_array{ref},
               index{idx}
         {}
 
@@ -127,7 +126,7 @@ class basic_sparse_set {
 
         [[nodiscard]] reference operator[](const difference_type value) const {
             const auto pos = size_type(index-value-1u);
-            return (*packed)[pos];
+            return (*packed_array)[pos];
         }
 
         [[nodiscard]] bool operator==(const sparse_set_iterator &other) const ENTT_NOEXCEPT {
@@ -156,7 +155,7 @@ class basic_sparse_set {
 
         [[nodiscard]] pointer operator->() const {
             const auto pos = size_type(index-1u);
-            return std::addressof((*packed)[pos]);
+            return std::addressof((*packed_array)[pos]);
         }
 
         [[nodiscard]] reference operator*() const {
@@ -164,7 +163,7 @@ class basic_sparse_set {
         }
 
     private:
-        const alloc_const_pointer *packed;
+        const alloc_const_pointer *packed_array;
         difference_type index;
     };
 
@@ -184,62 +183,62 @@ class basic_sparse_set {
 
             std::uninitialized_value_construct(mem + bucket, mem + sz);
 
-            if(sparse) {
-                std::uninitialized_copy(sparse, sparse + bucket, mem);
-                std::destroy(sparse, sparse + bucket);
-                alloc_ptr_traits::deallocate(allocator_ptr, sparse, bucket);
+            if(sparse_array) {
+                std::uninitialized_copy(sparse_array, sparse_array + bucket, mem);
+                std::destroy(sparse_array, sparse_array + bucket);
+                alloc_ptr_traits::deallocate(allocator_ptr, sparse_array, bucket);
             }
 
-            sparse = mem;
+            sparse_array = mem;
             bucket = sz;
         }
 
-        if(!sparse[idx]) {
-            sparse[idx] = alloc_traits::allocate(reserved.first(), sparse_page_v);
-            std::uninitialized_fill(sparse[idx], sparse[idx] + sparse_page_v, null);
+        if(!sparse_array[idx]) {
+            sparse_array[idx] = alloc_traits::allocate(reserved.first(), sparse_page_v);
+            std::uninitialized_fill(sparse_array[idx], sparse_array[idx] + sparse_page_v, null);
         }
 
-        return sparse[idx];
+        return sparse_array[idx];
     }
 
-    void resize_packed(const std::size_t req) {
+    void resize_packed_array(const std::size_t req) {
         auto &&[allocator, len] = reserved;
         ENTT_ASSERT((req != len) && !(req < count), "Invalid request");
         const auto mem = alloc_traits::allocate(allocator, req);
 
         std::uninitialized_fill(mem + count, mem + req, tombstone);
 
-        if(packed) {
-            std::uninitialized_copy(packed, packed + count, mem);
-            std::destroy(packed, packed + len);
-            alloc_traits::deallocate(allocator, packed, len);
+        if(packed_array) {
+            std::uninitialized_copy(packed_array, packed_array + count, mem);
+            std::destroy(packed_array, packed_array + len);
+            alloc_traits::deallocate(allocator, packed_array, len);
         }
 
-        packed = mem;
+        packed_array = mem;
         len = req;
     }
 
     void release_memory() {
         auto &&[allocator, len] = reserved;
 
-        if(packed) {
-            std::destroy(packed, packed + len);
-            alloc_traits::deallocate(allocator, packed, len);
+        if(packed_array) {
+            std::destroy(packed_array, packed_array + len);
+            alloc_traits::deallocate(allocator, packed_array, len);
         }
 
-        if(sparse) {
+        if(sparse_array) {
             alloc_ptr allocator_ptr{allocator};
 
             for(size_type pos{}; pos < bucket; ++pos) {
-                if(sparse[pos]) {
-                    std::destroy(sparse[pos], sparse[pos] + sparse_page_v);
-                    alloc_traits::deallocate(allocator, sparse[pos], sparse_page_v);
+                if(sparse_array[pos]) {
+                    std::destroy(sparse_array[pos], sparse_array[pos] + sparse_page_v);
+                    alloc_traits::deallocate(allocator, sparse_array[pos], sparse_page_v);
                 }
 
-                alloc_ptr_traits::destroy(allocator_ptr, std::addressof(sparse[pos]));
+                alloc_ptr_traits::destroy(allocator_ptr, std::addressof(sparse_array[pos]));
             }
 
-            alloc_ptr_traits::deallocate(allocator_ptr, sparse, bucket);
+            alloc_ptr_traits::deallocate(allocator_ptr, sparse_array, bucket);
         }
     }
 
@@ -255,13 +254,13 @@ protected:
      * @param entt A valid identifier.
      */
     virtual void swap_and_pop(const Entity entt, void *) {
-        auto &ref = sparse[page(entt)][offset(entt)];
+        auto &ref = sparse_array[page(entt)][offset(entt)];
         const auto pos = static_cast<size_type>(entity_traits::to_entity(ref));
-        ENTT_ASSERT(packed[pos] == entt, "Invalid identifier");
-        auto &last = packed[--count];
+        ENTT_ASSERT(packed_array[pos] == entt, "Invalid identifier");
+        auto &last = packed_array[--count];
 
-        packed[pos] = last;
-        auto &elem = sparse[page(last)][offset(last)];
+        packed_array[pos] = last;
+        auto &elem = sparse_array[page(last)][offset(last)];
         elem = entity_traits::combine(entity_traits::to_integral(ref), entity_traits::to_integral(elem));
         // lazy self-assignment guard
         ref = null;
@@ -274,11 +273,11 @@ protected:
      * @param entt A valid identifier.
      */
     virtual void in_place_pop(const Entity entt, void *) {
-        auto &ref = sparse[page(entt)][offset(entt)];
+        auto &ref = sparse_array[page(entt)][offset(entt)];
         const auto pos = static_cast<size_type>(entity_traits::to_entity(ref));
-        ENTT_ASSERT(packed[pos] == entt, "Invalid identifier");
+        ENTT_ASSERT(packed_array[pos] == entt, "Invalid identifier");
 
-        packed[pos] = std::exchange(free_list, entity_traits::combine(static_cast<typename entity_traits::entity_type>(pos), entity_traits::reserved));
+        packed_array[pos] = std::exchange(free_list, entity_traits::combine(static_cast<typename entity_traits::entity_type>(pos), entity_traits::reserved));
         // lazy self-assignment guard
         ref = null;
     }
@@ -315,12 +314,12 @@ public:
     /**
      * @brief Constructs an empty container with the given policy and allocator.
      * @param pol Type of deletion policy.
-     * @param allocator Allocator to use (possibly default-constructed).
+     * @param allocator The allocator to use (possibly default-constructed).
      */
     explicit basic_sparse_set(deletion_policy pol, const allocator_type &allocator = {})
         : reserved{allocator, size_type{}},
-          sparse{},
-          packed{},
+          sparse_array{},
+          packed_array{},
           bucket{},
           count{},
           free_list{tombstone},
@@ -332,9 +331,24 @@ public:
      * @param other The instance to move from.
      */
     basic_sparse_set(basic_sparse_set &&other) ENTT_NOEXCEPT
-        : reserved{std::move(other.reserved)},
-          sparse{std::exchange(other.sparse, alloc_ptr_pointer{})},
-          packed{std::exchange(other.packed, alloc_pointer{})},
+        : reserved{std::move(other.reserved.first()), std::exchange(other.reserved.second(), size_type{})},
+          sparse_array{std::exchange(other.sparse_array, alloc_ptr_pointer{})},
+          packed_array{std::exchange(other.packed_array, alloc_pointer{})},
+          bucket{std::exchange(other.bucket, size_type{})},
+          count{std::exchange(other.count, size_type{})},
+          free_list{std::exchange(other.free_list, tombstone)},
+          mode{other.mode}
+    {}
+
+    /**
+     * @brief Allocator-extended move constructor.
+     * @param other The instance to move from.
+     * @param allocator The allocator to use.
+     */
+    basic_sparse_set(basic_sparse_set &&other, const allocator_type &allocator) ENTT_NOEXCEPT
+        : reserved{allocator, std::exchange(other.reserved.second(), size_type{})},
+          sparse_array{std::exchange(other.sparse_array, alloc_ptr_pointer{})},
+          packed_array{std::exchange(other.packed_array, alloc_pointer{})},
           bucket{std::exchange(other.bucket, size_type{})},
           count{std::exchange(other.count, size_type{})},
           free_list{std::exchange(other.free_list, tombstone)},
@@ -354,9 +368,13 @@ public:
     basic_sparse_set & operator=(basic_sparse_set &&other) ENTT_NOEXCEPT {
         release_memory();
 
-        reserved = std::move(other.reserved);
-        sparse = std::exchange(other.sparse, alloc_ptr_pointer{});
-        packed = std::exchange(other.packed, alloc_pointer{});
+        if constexpr(alloc_traits::propagate_on_container_move_assignment::value) {
+            reserved.first() = std::move(other.reserved.first());
+        }
+
+        reserved.second() = std::exchange(other.reserved.second(), size_type{});
+        sparse_array = std::exchange(other.sparse_array, alloc_ptr_pointer{});
+        packed_array = std::exchange(other.packed_array, alloc_pointer{});
         bucket = std::exchange(other.bucket, size_type{});
         count = std::exchange(other.count, size_type{});
         free_list = std::exchange(other.free_list, tombstone);
@@ -365,6 +383,26 @@ public:
         return *this;
     }
 
+    /**
+     * @brief Exchanges the contents with those of a given sparse set.
+     * @param other Sparse set to exchange the content with.
+     */
+    void swap(basic_sparse_set &other) {
+        using std::swap;
+
+        if constexpr(alloc_traits::propagate_on_container_swap::value) {
+            swap(reserved.first(), other.reserved.first());
+        }
+
+        swap(reserved.second(), other.reserved.second());
+        swap(sparse_array, other.sparse_array);
+        swap(packed_array, other.packed_array);
+        swap(bucket, other.bucket);
+        swap(count, other.count);
+        swap(free_list, other.free_list);
+        swap(mode, other.mode);
+    }
+
     /**
      * @brief Returns the associated allocator.
      * @return The associated allocator.
@@ -399,7 +437,7 @@ public:
      */
     virtual void reserve(const size_type cap) {
         if(cap > reserved.second()) {
-            resize_packed(cap);
+            resize_packed_array(cap);
         }
     }
 
@@ -415,7 +453,7 @@ public:
     /*! @brief Requests the removal of unused capacity. */
     virtual void shrink_to_fit() {
         if(count < reserved.second()) {
-            resize_packed(count);
+            resize_packed_array(count);
         }
     }
 
@@ -460,7 +498,7 @@ public:
      * @return A pointer to the internal packed array.
      */
     [[nodiscard]] pointer data() const ENTT_NOEXCEPT {
-        return packed;
+        return packed_array;
     }
 
     /**
@@ -473,7 +511,7 @@ public:
      * @return An iterator to the first entity of the internal packed array.
      */
     [[nodiscard]] iterator begin() const ENTT_NOEXCEPT {
-        return iterator{std::addressof(packed), static_cast<typename entity_traits::difference_type>(count)};
+        return iterator{std::addressof(packed_array), static_cast<typename entity_traits::difference_type>(count)};
     }
 
     /**
@@ -487,7 +525,7 @@ public:
      * internal packed array.
      */
     [[nodiscard]] iterator end() const ENTT_NOEXCEPT {
-        return iterator{std::addressof(packed), {}};
+        return iterator{std::addressof(packed_array), {}};
     }
 
     /**
@@ -534,10 +572,10 @@ public:
      * @return True if the sparse set contains the entity, false otherwise.
      */
     [[nodiscard]] bool contains(const entity_type entt) const ENTT_NOEXCEPT {
-        if(const auto curr = page(entt); curr < bucket && sparse[curr]) {
+        if(const auto curr = page(entt); curr < bucket && sparse_array[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 (((~cap & entity_traits::to_integral(entt)) ^ entity_traits::to_integral(sparse_array[curr][offset(entt)])) < cap);
         }
 
         return false;
@@ -550,8 +588,8 @@ public:
      * 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)]);
+        if(const auto curr = page(entt); curr < bucket && sparse_array[curr]) {
+            return entity_traits::to_version(sparse_array[curr][offset(entt)]);
         }
 
         return entity_traits::to_version(tombstone);
@@ -569,7 +607,7 @@ public:
      */
     [[nodiscard]] size_type index(const entity_type entt) const ENTT_NOEXCEPT {
         ENTT_ASSERT(contains(entt), "Set does not contain entity");
-        return static_cast<size_type>(entity_traits::to_entity(sparse[page(entt)][offset(entt)]));
+        return static_cast<size_type>(entity_traits::to_entity(sparse_array[page(entt)][offset(entt)]));
     }
 
     /**
@@ -578,7 +616,7 @@ public:
      * @return The entity at specified location if any, a null entity otherwise.
      */
     [[nodiscard]] entity_type at(const size_type pos) const ENTT_NOEXCEPT {
-        return pos < count ? packed[pos] : null;
+        return pos < count ? packed_array[pos] : null;
     }
 
     /**
@@ -588,7 +626,7 @@ public:
      */
     [[nodiscard]] entity_type operator[](const size_type pos) const ENTT_NOEXCEPT {
         ENTT_ASSERT(pos < count, "Position is out of bounds");
-        return packed[pos];
+        return packed_array[pos];
     }
 
     /**
@@ -606,12 +644,12 @@ public:
 
         if(const auto len = reserved.second(); count == len) {
             const size_type sz = static_cast<size_type>(len * growth_factor_v);
-            resize_packed(sz + !(sz > len));
+            resize_packed_array(sz + !(sz > len));
         }
 
         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;
+        packed_array[count] = entt;
         return count++;
     }
 
@@ -632,7 +670,7 @@ public:
             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));
-            free_list = std::exchange(packed[pos], entt);
+            free_list = std::exchange(packed_array[pos], entt);
             return pos;
         }
     }
@@ -657,7 +695,7 @@ public:
             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;
+            packed_array[count++] = entt;
         }
     }
 
@@ -725,17 +763,17 @@ public:
     /*! @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(; next && packed_array[next - 1u] == tombstone; --next);
 
-        for(auto *it = &free_list; *it != null && next; it = std::addressof(packed[entity_traits::to_entity(*it)])) {
+        for(auto *it = &free_list; *it != null && next; it = std::addressof(packed_array[entity_traits::to_entity(*it)])) {
             if(const size_type pos = entity_traits::to_entity(*it); pos < next) {
                 --next;
                 move_and_pop(next, pos);
-                std::swap(packed[next], packed[pos]);
+                std::swap(packed_array[next], packed_array[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]));
+                sparse_array[page(packed_array[pos])][offset(packed_array[pos])] = entity_traits::combine(entity, entity_traits::to_integral(packed_array[pos]));
                 *it = entity_traits::combine(static_cast<typename entity_traits::entity_type>(next), entity_traits::reserved);
-                for(; next && packed[next - 1u] == tombstone; --next);
+                for(; next && packed_array[next - 1u] == tombstone; --next);
             }
         }
 
@@ -759,17 +797,17 @@ public:
     void swap(const entity_type lhs, const entity_type rhs) {
         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)];
+        auto &entt = sparse_array[page(lhs)][offset(lhs)];
+        auto &other = sparse_array[page(rhs)][offset(rhs)];
 
         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(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]);
+        entt = entity_traits::combine(to, entity_traits::to_integral(packed_array[from]));
+        other = entity_traits::combine(from, entity_traits::to_integral(packed_array[to]));
+        std::swap(packed_array[from], packed_array[to]);
     }
 
     /**
@@ -808,19 +846,19 @@ public:
         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)...);
+        algo(std::make_reverse_iterator(packed_array + length), std::make_reverse_iterator(packed_array), std::move(compare), std::forward<Args>(args)...);
 
         for(size_type pos{}; pos < length; ++pos) {
             auto curr = pos;
-            auto next = index(packed[curr]);
+            auto next = index(packed_array[curr]);
 
             while(curr != next) {
-                const auto idx = index(packed[next]);
-                const auto entt = packed[curr];
+                const auto idx = index(packed_array[next]);
+                const auto entt = packed_array[curr];
 
                 swap_at(next, idx);
                 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]));
+                sparse_array[page(entt)][offset(entt)] = entity_traits::combine(entity, entity_traits::to_integral(packed_array[curr]));
                 curr = std::exchange(next, idx);
             }
         }
@@ -866,9 +904,9 @@ public:
 
         for(size_type pos = count - 1; pos && from != to; ++from) {
             if(contains(*from)) {
-                if(*from != packed[pos]) {
+                if(*from != packed_array[pos]) {
                     // basic no-leak guarantee (with invalid state) if swapping throws
-                    swap(packed[pos], *from);
+                    swap(packed_array[pos], *from);
                 }
 
                 --pos;
@@ -893,8 +931,8 @@ public:
 
 private:
     compressed_pair<alloc, size_type> reserved;
-    alloc_ptr_pointer sparse;
-    alloc_pointer packed;
+    alloc_ptr_pointer sparse_array;
+    alloc_pointer packed_array;
     size_type bucket;
     size_type count;
     entity_type free_list;

+ 9 - 4
src/entt/entity/storage.hpp

@@ -348,7 +348,7 @@ public:
 
     /**
      * @brief Constructs an empty storage with a given allocator.
-     * @param allocator the allocator to use.
+     * @param allocator The allocator to use.
      */
     explicit basic_storage(const allocator_type &allocator)
         : base_type{deletion_policy{comp_traits::in_place_delete::value}, allocator},
@@ -703,11 +703,16 @@ public:
     /*! @brief Unsigned integer type. */
     using size_type = std::size_t;
 
+    /*! @brief Default constructor. */
+    basic_storage()
+        : basic_storage{allocator_type{}}
+    {}
+
     /**
-     * @brief Default constructor.
-     * @param allocator Allocator to use (possibly default-constructed).
+     * @brief Constructs an empty container with a given allocator.
+     * @param allocator The allocator to use.
      */
-    explicit basic_storage(const allocator_type &allocator = {})
+    explicit basic_storage(const allocator_type &allocator)
         : base_type{deletion_policy{comp_traits::in_place_delete::value}, allocator}
     {}
 

+ 57 - 0
test/entt/entity/custom_allocator.hpp

@@ -0,0 +1,57 @@
+#ifndef ENTT_ENTITY_CUSTOM_ALLOCATOR_HPP
+#define ENTT_ENTITY_CUSTOM_ALLOCATOR_HPP
+
+
+#include <cstddef>
+#include <memory>
+#include <type_traits>
+
+
+namespace test {
+
+
+template<typename Type>
+class custom_allocator: std::allocator<Type> {
+    template<typename Other>
+    friend class custom_allocator;
+
+    using base = std::allocator<Type>;
+
+public:
+    using value_type = Type;
+    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 propagate_on_container_swap = std::true_type;
+
+    custom_allocator() = default;
+
+    template<class Other>
+    custom_allocator(const custom_allocator<Other> &other)
+        : base{static_cast<const std::allocator<Other> &>(other)}
+    {}
+
+    pointer allocate(std::size_t length) {
+        return base::allocate(length);
+    }
+
+    void deallocate(pointer mem, std::size_t length) {
+        base::deallocate(mem, length);
+    }
+
+    bool operator==(const custom_allocator<Type> &) const {
+        return true;
+    }
+
+    bool operator!=(const custom_allocator<Type> &) const {
+        return false;
+    }
+};
+
+
+}
+
+
+#endif

+ 72 - 6
test/entt/entity/sparse_set.cpp

@@ -8,6 +8,7 @@
 #include <entt/entity/entity.hpp>
 #include <entt/entity/sparse_set.hpp>
 #include <entt/entity/fwd.hpp>
+#include "custom_allocator.hpp"
 #include "throwing_allocator.hpp"
 
 struct empty_type {};
@@ -187,6 +188,28 @@ TEST(SparseSet, Move) {
     ASSERT_EQ(other.at(0u), entt::entity{42});
 }
 
+TEST(SparseSet, Swap) {
+    entt::sparse_set set;
+    entt::sparse_set other{entt::deletion_policy::in_place};
+
+    set.emplace(entt::entity{42});
+
+    other.emplace(entt::entity{9});
+    other.emplace(entt::entity{3});
+    other.erase(entt::entity{9});
+
+    ASSERT_EQ(set.size(), 1u);
+    ASSERT_EQ(other.size(), 2u);
+
+    set.swap(other);
+
+    ASSERT_EQ(set.size(), 2u);
+    ASSERT_EQ(other.size(), 1u);
+
+    ASSERT_EQ(set.at(1u), entt::entity{3});
+    ASSERT_EQ(other.at(0u), entt::entity{42});
+}
+
 TEST(SparseSet, Pagination) {
     entt::sparse_set set;
 
@@ -710,7 +733,7 @@ TEST(SparseSet, Compact) {
     ASSERT_TRUE(set.empty());
 }
 
-TEST(SparseSet, Swap) {
+TEST(SparseSet, SwapEntity) {
     using traits_type = entt::entt_traits<entt::entity>;
 
     entt::sparse_set set;
@@ -1099,6 +1122,54 @@ TEST(SparseSet, CanModifyDuringIteration) {
     [[maybe_unused]] const auto entity = *it;
 }
 
+TEST(SparseSet, CustomAllocator) {
+    test::custom_allocator<entt::entity> allocator{};
+    entt::basic_sparse_set<entt::entity, test::custom_allocator<entt::entity>> set{allocator};
+
+    ASSERT_EQ(set.get_allocator(), allocator);
+
+    set.reserve(1u);
+
+    ASSERT_EQ(set.capacity(), 1u);
+
+    set.emplace(entt::entity{0});
+    set.emplace(entt::entity{1});
+
+    entt::basic_sparse_set<entt::entity, test::custom_allocator<entt::entity>> other{std::move(set), allocator};
+
+    ASSERT_TRUE(set.empty());
+    ASSERT_FALSE(other.empty());
+    ASSERT_EQ(set.capacity(), 0u);
+    ASSERT_EQ(other.capacity(), 2u);
+    ASSERT_EQ(other.size(), 2u);
+
+    set = std::move(other);
+
+    ASSERT_FALSE(set.empty());
+    ASSERT_TRUE(other.empty());
+    ASSERT_EQ(other.capacity(), 0u);
+    ASSERT_EQ(set.capacity(), 2u);
+    ASSERT_EQ(set.size(), 2u);
+
+    set.swap(other);
+    set = std::move(other);
+
+    ASSERT_FALSE(set.empty());
+    ASSERT_TRUE(other.empty());
+    ASSERT_EQ(other.capacity(), 0u);
+    ASSERT_EQ(set.capacity(), 2u);
+    ASSERT_EQ(set.size(), 2u);
+
+    set.clear();
+
+    ASSERT_EQ(set.capacity(), 2u);
+    ASSERT_EQ(set.size(), 0u);
+
+    set.shrink_to_fit();
+
+    ASSERT_EQ(set.capacity(), 0u);
+}
+
 TEST(SparseSet, ThrowingAllocator) {
     entt::basic_sparse_set<entt::entity, test::throwing_allocator<entt::entity>> set{};
 
@@ -1140,9 +1211,4 @@ TEST(SparseSet, ThrowingAllocator) {
     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){};
 }

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

@@ -11,10 +11,11 @@ namespace test {
 
 
 template<typename Type>
-class throwing_allocator {
+class throwing_allocator: std::allocator<Type> {
     template<typename Other>
     friend class throwing_allocator;
 
+    using base = std::allocator<Type>;
     struct test_exception {};
 
 public:
@@ -24,13 +25,14 @@ public:
     using void_pointer = void *;
     using const_void_pointer = const void *;
     using propagate_on_container_move_assignment = std::true_type;
+    using propagate_on_container_swap = std::true_type;
     using exception_type = test_exception;
 
     throwing_allocator() = default;
 
     template<class Other>
     throwing_allocator(const throwing_allocator<Other> &other)
-        : allocator{other.allocator}
+        : base{other}
     {}
 
     pointer allocate(std::size_t length) {
@@ -42,18 +44,15 @@ public:
         trigger_on_allocate = trigger_after_allocate;
         trigger_after_allocate = false;
 
-        return allocator.allocate(length);
+        return base::allocate(length);
     }
 
     void deallocate(pointer mem, std::size_t length) {
-        allocator.deallocate(mem, length);
+        base::deallocate(mem, length);
     }
 
     static inline bool trigger_on_allocate{};
     static inline bool trigger_after_allocate{};
-
-private:
-    std::allocator<Type> allocator;
 };