From 80c700b1f8abfb2a06c4d3cb4d52b6fafc0cd7b3 Mon Sep 17 00:00:00 2001 From: Michele Caini Date: Tue, 26 Apr 2022 11:43:59 +0200 Subject: [PATCH] sigh_storage_mixin: allocator support --- TODO | 3 +- src/entt/entity/sigh_storage_mixin.hpp | 77 +++++- test/entt/entity/sigh_storage_mixin.cpp | 348 +++++++++++++++++++++++- 3 files changed, 409 insertions(+), 19 deletions(-) diff --git a/TODO b/TODO index 5ab0dd050..2af92f545 100644 --- a/TODO +++ b/TODO @@ -11,6 +11,7 @@ DOC: * examples (and credits) from @alanjfs :) WIP: +* remove storage::base_type, make views extract the sparse set directly * make non-const registry::get use const assure or the like * emitter: runtime handlers, allocator support (ready for both already) * view/group: no storage_traits dependency -> use storage instead of components for the definition @@ -20,7 +21,7 @@ WIP: * iterator based try_emplace vs try_insert for perf reasons * dedicated entity storage, in-place O(1) release/destroy for non-orphaned entities, out-of-sync model * entity-only and exclude-only views -* custom allocators all over (sigh storage mixin, registry, ...) +* custom allocators all over (registry, ...) * consider removing ENTT_NOEXCEPT, use ENTT_NOEXCEPT_IF (or noexcept(...)) as appropriate in any case (ie make compressed_pair conditionally noexcept) * add test for maximum number of entities reached * add user data to type_info diff --git a/src/entt/entity/sigh_storage_mixin.hpp b/src/entt/entity/sigh_storage_mixin.hpp index 39e7226f8..36659f9ba 100644 --- a/src/entt/entity/sigh_storage_mixin.hpp +++ b/src/entt/entity/sigh_storage_mixin.hpp @@ -24,6 +24,7 @@ namespace entt { */ template class sigh_storage_mixin final: public Type { + using sigh_type = sigh &, const typename Type::entity_type), typename Type::allocator_type>; using basic_iterator = typename Type::basic_iterator; template @@ -54,11 +55,75 @@ class sigh_storage_mixin final: public Type { } public: + /*! @brief Allocator type. */ + using allocator_type = typename Type::allocator_type; /*! @brief Underlying entity identifier. */ using entity_type = typename Type::entity_type; - /*! @brief Inherited constructors. */ - using Type::Type; + /*! @brief Default constructor. */ + sigh_storage_mixin() + : sigh_storage_mixin{allocator_type{}} {} + + /** + * @brief Constructs an empty storage with a given allocator. + * @param allocator The allocator to use. + */ + explicit sigh_storage_mixin(const allocator_type &allocator) + : Type{allocator}, + owner{}, + construction{}, + destruction{}, + update{} {} + + /** + * @brief Move constructor. + * @param other The instance to move from. + */ + sigh_storage_mixin(sigh_storage_mixin &&other) ENTT_NOEXCEPT + : Type{std::move(other)}, + owner{other.owner}, + construction{std::move(other.construction)}, + destruction{std::move(other.destruction)}, + update{std::move(other.update)} {} + + /** + * @brief Allocator-extended move constructor. + * @param other The instance to move from. + * @param allocator The allocator to use. + */ + sigh_storage_mixin(sigh_storage_mixin &&other, const allocator_type &allocator) ENTT_NOEXCEPT + : Type{std::move(other), allocator}, + owner{other.owner}, + construction{std::move(other.construction), allocator}, + destruction{std::move(other.destruction), allocator}, + update{std::move(other.update), allocator} {} + + /** + * @brief Move assignment operator. + * @param other The instance to move from. + * @return This storage. + */ + sigh_storage_mixin &operator=(sigh_storage_mixin &&other) ENTT_NOEXCEPT { + Type::operator=(std::move(other)); + owner = other.owner; + construction = std::move(other.construction); + destruction = std::move(other.destruction); + update = std::move(other.update); + return *this; + } + + /** + * @brief Exchanges the contents with those of a given storage. + * @param other Storage to exchange the content with. + */ + void swap(sigh_storage_mixin &other) { + using std::swap; + Type::swap(other); + swap(owner, other.owner); + swap(construction, other.construction); + swap(destruction, other.destruction); + swap(update, other.update); + } /** * @brief Returns a sink object. @@ -166,10 +231,10 @@ public: } private: - sigh &, const entity_type)> construction{}; - sigh &, const entity_type)> destruction{}; - sigh &, const entity_type)> update{}; - basic_registry *owner{}; + basic_registry *owner; + sigh_type construction; + sigh_type destruction; + sigh_type update; }; } // namespace entt diff --git a/test/entt/entity/sigh_storage_mixin.cpp b/test/entt/entity/sigh_storage_mixin.cpp index 03b72413c..5c93bc5ad 100644 --- a/test/entt/entity/sigh_storage_mixin.cpp +++ b/test/entt/entity/sigh_storage_mixin.cpp @@ -2,6 +2,8 @@ #include #include #include +#include "../common/throwing_allocator.hpp" +#include "../common/throwing_type.hpp" struct empty_type {}; @@ -31,13 +33,12 @@ TEST(SighStorageMixin, GenericType) { entt::entity entities[2u]{entt::entity{3}, entt::entity{42}}; entt::sigh_storage_mixin> pool; entt::sparse_set &base = pool; - entt::registry registry{}; - - pool.bind(entt::forward_as_any(registry)); + entt::registry registry; counter on_construct{}; counter on_destroy{}; + pool.bind(entt::forward_as_any(registry)); pool.on_construct().connect<&listener>(on_construct); pool.on_destroy().connect<&listener>(on_destroy); @@ -97,13 +98,12 @@ TEST(SighStorageMixin, StableType) { entt::entity entities[2u]{entt::entity{3}, entt::entity{42}}; entt::sigh_storage_mixin> pool; entt::sparse_set &base = pool; - entt::registry registry{}; - - pool.bind(entt::forward_as_any(registry)); + entt::registry registry; counter on_construct{}; counter on_destroy{}; + pool.bind(entt::forward_as_any(registry)); pool.on_construct().connect<&listener>(on_construct); pool.on_destroy().connect<&listener>(on_destroy); @@ -163,13 +163,12 @@ TEST(SighStorageMixin, EmptyType) { entt::entity entities[2u]{entt::entity{3}, entt::entity{42}}; entt::sigh_storage_mixin> pool; entt::sparse_set &base = pool; - entt::registry registry{}; - - pool.bind(entt::forward_as_any(registry)); + entt::registry registry; counter on_construct{}; counter on_destroy{}; + pool.bind(entt::forward_as_any(registry)); pool.on_construct().connect<&listener>(on_construct); pool.on_destroy().connect<&listener>(on_destroy); @@ -229,13 +228,12 @@ TEST(SighStorageMixin, NonDefaultConstructibleType) { entt::entity entities[2u]{entt::entity{3}, entt::entity{42}}; entt::sigh_storage_mixin> pool; entt::sparse_set &base = pool; - entt::registry registry{}; - - pool.bind(entt::forward_as_any(registry)); + entt::registry registry; counter on_construct{}; counter on_destroy{}; + pool.bind(entt::forward_as_any(registry)); pool.on_construct().connect<&listener>(on_construct); pool.on_destroy().connect<&listener>(on_destroy); @@ -281,3 +279,329 @@ TEST(SighStorageMixin, NonDefaultConstructibleType) { ASSERT_EQ(on_destroy.value, 3); ASSERT_TRUE(pool.empty()); } + +TEST(SighStorageMixin, VoidType) { + entt::sigh_storage_mixin> pool; + entt::registry registry; + + counter on_construct{}; + counter on_destroy{}; + + pool.bind(entt::forward_as_any(registry)); + pool.on_construct().connect<&listener>(on_construct); + pool.on_destroy().connect<&listener>(on_destroy); + + pool.emplace(entt::entity{99}); + + ASSERT_EQ(pool.type(), entt::type_id()); + ASSERT_TRUE(pool.contains(entt::entity{99})); + + entt::sigh_storage_mixin> other{std::move(pool)}; + + ASSERT_FALSE(pool.contains(entt::entity{99})); + ASSERT_TRUE(other.contains(entt::entity{99})); + + pool = std::move(other); + + ASSERT_TRUE(pool.contains(entt::entity{99})); + ASSERT_FALSE(other.contains(entt::entity{99})); + + pool.clear(); + + ASSERT_EQ(on_construct.value, 1); + ASSERT_EQ(on_destroy.value, 1); +} + +TEST(SighStorageMixin, Move) { + entt::sigh_storage_mixin> pool; + entt::registry registry; + + counter on_construct{}; + counter on_destroy{}; + + pool.bind(entt::forward_as_any(registry)); + pool.on_construct().connect<&listener>(on_construct); + pool.on_destroy().connect<&listener>(on_destroy); + + pool.emplace(entt::entity{3}, 3); + + ASSERT_TRUE(std::is_move_constructible_v); + ASSERT_TRUE(std::is_move_assignable_v); + ASSERT_EQ(pool.type(), entt::type_id()); + + entt::sigh_storage_mixin> other{std::move(pool)}; + + ASSERT_TRUE(pool.empty()); + ASSERT_FALSE(other.empty()); + ASSERT_EQ(other.type(), entt::type_id()); + ASSERT_EQ(pool.at(0u), static_cast(entt::null)); + ASSERT_EQ(other.at(0u), entt::entity{3}); + ASSERT_EQ(other.get(entt::entity{3}), 3); + + pool = std::move(other); + + ASSERT_FALSE(pool.empty()); + ASSERT_TRUE(other.empty()); + ASSERT_EQ(pool.at(0u), entt::entity{3}); + ASSERT_EQ(pool.get(entt::entity{3}), 3); + ASSERT_EQ(other.at(0u), static_cast(entt::null)); + + other = entt::sigh_storage_mixin>{}; + other.bind(entt::forward_as_any(registry)); + + other.emplace(entt::entity{42}, 42); + other = std::move(pool); + + ASSERT_TRUE(pool.empty()); + ASSERT_FALSE(other.empty()); + ASSERT_EQ(pool.at(0u), static_cast(entt::null)); + ASSERT_EQ(other.at(0u), entt::entity{3}); + ASSERT_EQ(other.get(entt::entity{3}), 3); + + other.clear(); + + ASSERT_EQ(on_construct.value, 1); + ASSERT_EQ(on_destroy.value, 1); +} + +TEST(SighStorageMixin, Swap) { + entt::sigh_storage_mixin> pool; + entt::sigh_storage_mixin> other; + entt::registry registry; + + counter on_construct{}; + counter on_destroy{}; + + pool.bind(entt::forward_as_any(registry)); + pool.on_construct().connect<&listener>(on_construct); + pool.on_destroy().connect<&listener>(on_destroy); + + other.bind(entt::forward_as_any(registry)); + other.on_construct().connect<&listener>(on_construct); + other.on_destroy().connect<&listener>(on_destroy); + + pool.emplace(entt::entity{42}, 41); + + other.emplace(entt::entity{9}, 8); + other.emplace(entt::entity{3}, 2); + other.erase(entt::entity{9}); + + ASSERT_EQ(pool.size(), 1u); + ASSERT_EQ(other.size(), 1u); + + pool.swap(other); + + ASSERT_EQ(pool.type(), entt::type_id()); + ASSERT_EQ(other.type(), entt::type_id()); + + ASSERT_EQ(pool.size(), 1u); + ASSERT_EQ(other.size(), 1u); + + ASSERT_EQ(pool.at(0u), entt::entity{3}); + ASSERT_EQ(pool.get(entt::entity{3}), 2); + + ASSERT_EQ(other.at(0u), entt::entity{42}); + ASSERT_EQ(other.get(entt::entity{42}), 41); + + pool.clear(); + other.clear(); + + ASSERT_EQ(on_construct.value, 3); + ASSERT_EQ(on_destroy.value, 3); +} + +TEST(SighStorageMixin, CustomAllocator) { + auto test = [](auto pool, auto alloc) { + entt::registry registry; + + counter on_construct{}; + counter on_destroy{}; + + pool.bind(entt::forward_as_any(registry)); + pool.on_construct().connect<&listener>(on_construct); + pool.on_destroy().connect<&listener>(on_destroy); + + pool.reserve(1u); + + ASSERT_NE(pool.capacity(), 0u); + + pool.emplace(entt::entity{0}); + pool.emplace(entt::entity{1}); + + decltype(pool) other{std::move(pool), alloc}; + + ASSERT_TRUE(pool.empty()); + ASSERT_FALSE(other.empty()); + ASSERT_EQ(pool.capacity(), 0u); + ASSERT_NE(other.capacity(), 0u); + ASSERT_EQ(other.size(), 2u); + + pool = std::move(other); + + ASSERT_FALSE(pool.empty()); + ASSERT_TRUE(other.empty()); + ASSERT_EQ(other.capacity(), 0u); + ASSERT_NE(pool.capacity(), 0u); + ASSERT_EQ(pool.size(), 2u); + + pool.swap(other); + pool = std::move(other); + + ASSERT_FALSE(pool.empty()); + ASSERT_TRUE(other.empty()); + ASSERT_EQ(other.capacity(), 0u); + ASSERT_NE(pool.capacity(), 0u); + ASSERT_EQ(pool.size(), 2u); + + pool.clear(); + + ASSERT_NE(pool.capacity(), 0u); + ASSERT_EQ(pool.size(), 0u); + + ASSERT_EQ(on_construct.value, 2); + ASSERT_EQ(on_destroy.value, 2); + }; + + test::throwing_allocator allocator{}; + + test(entt::sigh_storage_mixin>>{allocator}, allocator); + test(entt::sigh_storage_mixin>>{allocator}, allocator); + test(entt::sigh_storage_mixin>>{allocator}, allocator); +} + +TEST(SighStorageMixin, ThrowingAllocator) { + auto test = [](auto pool) { + using pool_allocator_type = typename decltype(pool)::allocator_type; + using value_type = typename decltype(pool)::value_type; + + typename std::decay_t::base_type &base = pool; + entt::registry registry; + + counter on_construct{}; + counter on_destroy{}; + + pool.bind(entt::forward_as_any(registry)); + pool.on_construct().connect<&listener>(on_construct); + pool.on_destroy().connect<&listener>(on_destroy); + + pool_allocator_type::trigger_on_allocate = true; + + ASSERT_THROW(pool.reserve(1u), typename pool_allocator_type::exception_type); + ASSERT_EQ(pool.capacity(), 0u); + + pool_allocator_type::trigger_after_allocate = true; + + ASSERT_THROW(pool.reserve(2 * ENTT_PACKED_PAGE), typename pool_allocator_type::exception_type); + ASSERT_EQ(pool.capacity(), ENTT_PACKED_PAGE); + + pool.shrink_to_fit(); + + ASSERT_EQ(pool.capacity(), 0u); + + test::throwing_allocator::trigger_on_allocate = true; + + 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; + + ASSERT_THROW(base.emplace(entt::entity{0}), test::throwing_allocator::exception_type); + ASSERT_FALSE(base.contains(entt::entity{0})); + ASSERT_TRUE(base.empty()); + + pool_allocator_type::trigger_on_allocate = true; + + ASSERT_THROW(pool.emplace(entt::entity{0}, 0), typename pool_allocator_type::exception_type); + ASSERT_FALSE(pool.contains(entt::entity{0})); + ASSERT_NO_THROW(pool.compact()); + 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; + + ASSERT_THROW(pool.insert(std::begin(entities), std::end(entities), value_type{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 value_type components[2u]{value_type{1}, value_type{ENTT_SPARSE_PAGE}}; + test::throwing_allocator::trigger_on_allocate = true; + pool.compact(); + + 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})); + + ASSERT_EQ(on_construct.value, 1); + ASSERT_EQ(on_destroy.value, 1); + }; + + test(entt::sigh_storage_mixin>>{}); + test(entt::sigh_storage_mixin>>{}); +} + +TEST(SighStorageMixin, ThrowingComponent) { + entt::sigh_storage_mixin> pool; + entt::registry registry; + + counter on_construct{}; + counter on_destroy{}; + + pool.bind(entt::forward_as_any(registry)); + pool.on_construct().connect<&listener>(on_construct); + pool.on_destroy().connect<&listener>(on_destroy); + + test::throwing_type::trigger_on_value = 42; + + // strong exception safety + ASSERT_THROW(pool.emplace(entt::entity{0}, test::throwing_type{42}), typename test::throwing_type::exception_type); + ASSERT_TRUE(pool.empty()); + + const entt::entity entities[2u]{entt::entity{42}, entt::entity{1}}; + const test::throwing_type components[2u]{42, 1}; + + // basic exception safety + ASSERT_THROW(pool.insert(std::begin(entities), std::end(entities), test::throwing_type{42}), typename test::throwing_type::exception_type); + ASSERT_EQ(pool.size(), 0u); + ASSERT_FALSE(pool.contains(entt::entity{1})); + + // basic exception safety + ASSERT_THROW(pool.insert(std::begin(entities), std::end(entities), std::begin(components)), typename test::throwing_type::exception_type); + ASSERT_EQ(pool.size(), 0u); + ASSERT_FALSE(pool.contains(entt::entity{1})); + + // basic exception safety + ASSERT_THROW(pool.insert(std::rbegin(entities), std::rend(entities), std::rbegin(components)), typename test::throwing_type::exception_type); + ASSERT_EQ(pool.size(), 1u); + ASSERT_TRUE(pool.contains(entt::entity{1})); + ASSERT_EQ(pool.get(entt::entity{1}), 1); + + pool.clear(); + 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_type::exception_type); + 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_type::moved_from_value); + + test::throwing_type::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.get(entt::entity{42}), 42); + + ASSERT_EQ(on_construct.value, 2); + ASSERT_EQ(on_destroy.value, 3); +}