diff --git a/src/entt/entity/component.hpp b/src/entt/entity/component.hpp new file mode 100644 index 000000000..d17b1c3e5 --- /dev/null +++ b/src/entt/entity/component.hpp @@ -0,0 +1,29 @@ +#ifndef ENTT_ENTITY_COMPONENT_HPP +#define ENTT_ENTITY_COMPONENT_HPP + + +#include +#include "../config/config.h" + + +namespace entt { + + +/** + * @brief Common way to access various properties of components + * @tparam Type Type of component. + */ +template +struct component_traits { + static_assert(std::is_same_v, 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 diff --git a/src/entt/entity/fwd.hpp b/src/entt/entity/fwd.hpp index 0244820c2..9b079f922 100644 --- a/src/entt/entity/fwd.hpp +++ b/src/entt/entity/fwd.hpp @@ -13,8 +13,8 @@ template> class basic_sparse_set; -template, typename = void> -class basic_storage; +template> +struct basic_storage; template diff --git a/src/entt/entity/sparse_set.hpp b/src/entt/entity/sparse_set.hpp index d329e9fa9..05e6bbb5a 100644 --- a/src/entt/entity/sparse_set.hpp +++ b/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; - - 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); - - ENTT_TRY { - assure_page(page(entt))[offset(entt)] = entity_type{static_cast(count)}; - } ENTT_CATCH { - alloc_traits::destroy(allocator, std::addressof(packed[count])); - ENTT_THROW; - } - - ++count; - } - - void pop(const Entity entt, void *ud) { - ENTT_ASSERT(contains(entt), "Set does not contain entity"); + void stable_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)}; - auto &last = packed[count - 1u]; + ENTT_ASSERT(packed[pos] == entt, "Invalid entity identifier"); - // 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] = std::exchange(free_list, (tombstone | entity_type{static_cast(pos)})); ref = null; - // don't expect exceptions here, instead allow for nosy destructors - alloc_traits::destroy(allocator, std::addressof(packed[--count])); - swap_and_pop(pos); + // strong exception guarantee + in_place_pop(pos); + } + + void unstable_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); + + 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]; + + 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; + + 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(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; /** - * @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(reserved * growth_factor); - resize_packed(sz + !(sz > reserved)); - } + if(free_list == null) { + if(count == reserved) { + const size_type sz = static_cast(reserved * growth_factor); + resize_packed(sz + !(sz > reserved)); + } - push_back(entt); + assure_page(page(entt))[offset(entt)] = entity_type{static_cast(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(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(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 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(pos)}; + *it = (tombstone | entity_type{static_cast(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)...); 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(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; }; diff --git a/src/entt/entity/storage.hpp b/src/entt/entity/storage.hpp index c543f5b36..08bc06a70 100644 --- a/src/entt/entity/storage.hpp +++ b/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 -class basic_storage: public basic_sparse_set::template rebind_alloc> { - static_assert(std::is_move_constructible_v && std::is_move_assignable_v, "The managed type must be at least move constructible and assignable"); - +template +class basic_storage_impl: public basic_sparse_set::template rebind_alloc> { static constexpr auto packed_page = ENTT_PACKED_PAGE; + using comp_traits = component_traits; + using underlying_type = basic_sparse_set::template rebind_alloc>; - using traits_type = entt_traits; + using difference_type = typename entt_traits::difference_type; using alloc_traits = typename std::allocator_traits::template rebind_traits; using alloc_pointer = typename alloc_traits::pointer; @@ -70,15 +71,8 @@ class basic_storage: public basic_sparse_set - class storage_iterator final { - friend class basic_storage; - - 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 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 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 - 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; + using iterator = storage_iterator; /*! @brief Constant random access iterator type. */ - using const_iterator = storage_iterator; + using const_iterator = storage_iterator; /*! @brief Reverse iterator type. */ using reverse_iterator = std::reverse_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(std::as_const(*this).get(entt)); } @@ -696,12 +688,18 @@ private: }; -/*! @copydoc basic_storage */ +/*! @copydoc basic_storage_impl */ template -class basic_storage>>>: public basic_sparse_set::template rebind_alloc> { +class basic_storage_impl::ignore_if_empty::value && std::is_empty_v>> + : public basic_sparse_set::template rebind_alloc> +{ + using comp_traits = component_traits; using underlying_type = basic_sparse_set::template rebind_alloc>; + using alloc_traits = typename std::allocator_traits::template rebind_traits; 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 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 *>(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 +struct basic_storage: basic_storage_impl { + using basic_storage_impl::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 struct storage_traits { diff --git a/src/entt/entt.hpp b/src/entt/entt.hpp index 03add284d..524816980 100644 --- a/src/entt/entt.hpp +++ b/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" diff --git a/test/entt/entity/sparse_set.cpp b/test/entt/entity/sparse_set.cpp index 96c21a4b3..c6475de31 100644 --- a/test/entt/entity/sparse_set.cpp +++ b/test/entt/entity/sparse_set.cpp @@ -5,10 +5,10 @@ #include #include #include +#include #include #include #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::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 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> set{}; @@ -615,6 +867,13 @@ TEST(SparseSet, ThrowingAllocator) { ASSERT_EQ(set.capacity(), 0u); ASSERT_EQ(set.extent(), 0u); + test::throwing_allocator::trigger_on_allocate = true; + + // strong exception safety + ASSERT_THROW(set.emplace(entt::entity{0}), test::throwing_allocator::exception_type); + ASSERT_EQ(set.capacity(), 0u); + ASSERT_EQ(set.extent(), 0u); + set.emplace(entt::entity{0}); test::throwing_allocator::trigger_on_allocate = true; @@ -624,36 +883,24 @@ TEST(SparseSet, ThrowingAllocator) { ASSERT_EQ(set.extent(), ENTT_SPARSE_PAGE); ASSERT_TRUE(set.contains(entt::entity{0})); - test::throwing_allocator::trigger_on_pointer_copy = true; + entt::entity entities[2u]{entt::entity{1}, entt::entity{ENTT_SPARSE_PAGE}}; + test::throwing_allocator::trigger_after_allocate = true; - // strong exception safety - ASSERT_THROW(set.reserve(2u), test::throwing_allocator::exception_type); - ASSERT_EQ(set.capacity(), 1u); - ASSERT_EQ(set.extent(), ENTT_SPARSE_PAGE); + // basic exception safety + ASSERT_THROW(set.insert(std::begin(entities), std::end(entities)), test::throwing_allocator::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.reserve(ENTT_PACKED_PAGE); - test::throwing_allocator::trigger_on_pointer_copy = true; + set.emplace(entities[1u]); - // strong exception safety - ASSERT_THROW(set.emplace(entt::entity{ENTT_SPARSE_PAGE + 1u}), test::throwing_allocator::exception_type); - ASSERT_EQ(set.capacity(), ENTT_PACKED_PAGE); - ASSERT_EQ(set.extent(), ENTT_SPARSE_PAGE); - ASSERT_TRUE(set.contains(entt::entity{0})); + 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::trigger_on_pointer_copy = true; - - // strong exception safety - ASSERT_THROW(set.emplace(entt::entity{0}), test::throwing_allocator::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})); } diff --git a/test/entt/entity/storage.cpp b/test/entt/entity/storage.cpp index c84a126ae..00e36945d 100644 --- a/test/entt/entity/storage.cpp +++ b/test/entt/entity/storage.cpp @@ -5,14 +5,21 @@ #include #include #include -#include +#include #include +#include #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 { + 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 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 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 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 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 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 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 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 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 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 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::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::exception_type); ASSERT_EQ(pool.capacity(), 0u); - test::throwing_allocator::trigger_on_pointer_copy = true; - - // strong exception safety - ASSERT_THROW(pool.reserve(1u), test::throwing_allocator::exception_type); - ASSERT_EQ(pool.capacity(), 0u); - - pool.reserve(2 * ENTT_PACKED_PAGE); - test::throwing_allocator::pointer>::trigger_on_pointer_copy = true; - - // strong exception safety - ASSERT_THROW(pool.shrink_to_fit(), test::throwing_allocator::pointer>::exception_type); - ASSERT_EQ(pool.capacity(), 2 * ENTT_PACKED_PAGE); - pool.shrink_to_fit(); test::throwing_allocator::trigger_on_allocate = true; @@ -892,4 +1119,29 @@ TEST(Storage, ThrowingAllocator) { ASSERT_THROW(pool.emplace(entt::entity{0}, 0), test::throwing_allocator::exception_type); ASSERT_FALSE(pool.contains(entt::entity{0})); ASSERT_TRUE(pool.empty()); + + test::throwing_allocator::trigger_on_allocate = true; + + // strong exception safety + ASSERT_THROW(pool.emplace(entt::entity{0}, 0), test::throwing_allocator::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::trigger_after_allocate = true; + + // basic exception safety + ASSERT_THROW(pool.insert(std::begin(entities), std::end(entities), 0), test::throwing_allocator::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::trigger_on_allocate = true; + + // basic exception safety + ASSERT_THROW(pool.insert(std::begin(entities), std::end(entities), std::begin(components)), test::throwing_allocator::exception_type); + ASSERT_TRUE(pool.contains(entt::entity{1})); + ASSERT_FALSE(pool.contains(entt::entity{ENTT_SPARSE_PAGE})); } diff --git a/test/entt/entity/throwing_allocator.hpp b/test/entt/entity/throwing_allocator.hpp index eb5d35db4..f15781a7a 100644 --- a/test/entt/entity/throwing_allocator.hpp +++ b/test/entt/entity/throwing_allocator.hpp @@ -3,7 +3,6 @@ #include -#include #include #include @@ -16,123 +15,18 @@ class throwing_allocator { template friend class throwing_allocator; - struct fancy_pointer final { - using difference_type = typename std::iterator_traits::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 throwing_allocator(const throwing_allocator &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 allocator; diff --git a/test/entt/entity/throwing_component.hpp b/test/entt/entity/throwing_component.hpp index 71b3ac217..878706c28 100644 --- a/test/entt/entity/throwing_component.hpp +++ b/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{}; } diff --git a/test/entt/entity/throwing_entity.hpp b/test/entt/entity/throwing_entity.hpp deleted file mode 100644 index 0ee834dfb..000000000 --- a/test/entt/entity/throwing_entity.hpp +++ /dev/null @@ -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