diff --git a/src/entt/entity/sparse_set.hpp b/src/entt/entity/sparse_set.hpp index 639852ef2..d329e9fa9 100644 --- a/src/entt/entity/sparse_set.hpp +++ b/src/entt/entity/sparse_set.hpp @@ -47,20 +47,18 @@ class basic_sparse_set { using traits_type = entt_traits; - using alloc_type = typename std::allocator_traits::template rebind_alloc; - using alloc_traits = std::allocator_traits; + using alloc_traits = typename std::allocator_traits::template rebind_traits; using alloc_pointer = typename alloc_traits::pointer; using alloc_const_pointer = typename alloc_traits::const_pointer; - using bucket_alloc_type = typename std::allocator_traits::template rebind_alloc; - using bucket_alloc_traits = std::allocator_traits; + using bucket_alloc_traits = typename std::allocator_traits::template rebind_traits; using bucket_alloc_pointer = typename bucket_alloc_traits::pointer; 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; + friend class basic_sparse_set; using index_type = typename traits_type::difference_type; @@ -189,7 +187,7 @@ class basic_sparse_set { std::destroy(sparse, sparse + bucket); bucket_alloc_traits::deallocate(bucket_allocator, sparse, bucket); - + sparse = mem; bucket = sz; } @@ -302,7 +300,7 @@ protected: public: /*! @brief Allocator type. */ - using allocator_type = alloc_type; + using allocator_type = typename alloc_traits::allocator_type; /*! @brief Underlying entity identifier. */ using entity_type = Entity; /*! @brief Unsigned integer type. */ @@ -450,7 +448,7 @@ public: * @return An iterator to the first entity of the internal packed array. */ [[nodiscard]] iterator begin() const ENTT_NOEXCEPT { - return iterator{&packed, static_cast(count)}; + return iterator{std::addressof(packed), static_cast(count)}; } /** @@ -464,7 +462,7 @@ public: * internal packed array. */ [[nodiscard]] iterator end() const ENTT_NOEXCEPT { - return iterator{&packed, {}}; + return iterator{std::addressof(packed), {}}; } /** @@ -785,8 +783,8 @@ public: } private: - alloc_type allocator; - bucket_alloc_type bucket_allocator; + typename alloc_traits::allocator_type allocator; + typename bucket_alloc_traits::allocator_type bucket_allocator; bucket_alloc_pointer sparse; alloc_pointer packed; std::size_t bucket; diff --git a/src/entt/entity/storage.hpp b/src/entt/entity/storage.hpp index 7cb9f3b99..d7f2895c0 100644 --- a/src/entt/entity/storage.hpp +++ b/src/entt/entity/storage.hpp @@ -53,29 +53,25 @@ class basic_storage: public basic_sparse_set; + using underlying_type = basic_sparse_set::template rebind_alloc>; using traits_type = entt_traits; - using alloc_type = typename std::allocator_traits::template rebind_alloc; - using alloc_traits = std::allocator_traits; + using alloc_traits = typename std::allocator_traits::template rebind_traits; using alloc_pointer = typename alloc_traits::pointer; using alloc_const_pointer = typename alloc_traits::const_pointer; - using bucket_alloc_type = typename std::allocator_traits::template rebind_alloc; - using bucket_alloc_traits = std::allocator_traits; + using bucket_alloc_traits = typename std::allocator_traits::template rebind_traits; using bucket_alloc_pointer = typename bucket_alloc_traits::pointer; using bucket_alloc_const_type = typename std::allocator_traits::template rebind_alloc; using bucket_alloc_const_pointer = typename std::allocator_traits::const_pointer; - using entity_alloc_type = typename std::allocator_traits::template rebind_alloc; - static_assert(alloc_traits::propagate_on_container_move_assignment::value); static_assert(bucket_alloc_traits::propagate_on_container_move_assignment::value); template class storage_iterator final { - friend class basic_storage; + friend class basic_storage; storage_iterator(bucket_alloc_pointer const *ref, const typename traits_type::difference_type idx) ENTT_NOEXCEPT : packed{ref}, index{idx} @@ -211,7 +207,7 @@ class basic_storage: public basic_sparse_set{alloc}, + : underlying_type{alloc}, allocator{alloc}, bucket_allocator{alloc}, packed{}, @@ -332,7 +328,7 @@ public: * @param other The instance to move from. */ basic_storage(basic_storage &&other) ENTT_NOEXCEPT - : basic_sparse_set{std::move(other)}, + : underlying_type{std::move(other)}, allocator{std::move(other.allocator)}, bucket_allocator{std::move(other.bucket_allocator)}, packed{std::exchange(other.packed, bucket_alloc_pointer{})}, @@ -352,7 +348,7 @@ public: basic_storage & operator=(basic_storage &&other) ENTT_NOEXCEPT { release_memory(); - basic_sparse_set::operator=(std::move(other)); + underlying_type::operator=(std::move(other)); allocator = std::move(other.allocator); bucket_allocator = std::move(other.bucket_allocator); @@ -416,7 +412,7 @@ public: */ [[nodiscard]] const_iterator cbegin() const ENTT_NOEXCEPT { const typename traits_type::difference_type pos = underlying_type::size(); - return const_iterator{&packed, pos}; + return const_iterator{std::addressof(packed), pos}; } /*! @copydoc cbegin */ @@ -427,7 +423,7 @@ public: /*! @copydoc begin */ [[nodiscard]] iterator begin() ENTT_NOEXCEPT { const typename traits_type::difference_type pos = underlying_type::size(); - return iterator{&packed, pos}; + return iterator{std::addressof(packed), pos}; } /** @@ -441,7 +437,7 @@ public: * internal array. */ [[nodiscard]] const_iterator cend() const ENTT_NOEXCEPT { - return const_iterator{&packed, {}}; + return const_iterator{std::addressof(packed), {}}; } /*! @copydoc cend */ @@ -451,7 +447,7 @@ public: /*! @copydoc end */ [[nodiscard]] iterator end() ENTT_NOEXCEPT { - return iterator{&packed, {}}; + return iterator{std::addressof(packed), {}}; } /** @@ -693,8 +689,8 @@ public: } private: - alloc_type allocator; - bucket_alloc_type bucket_allocator; + typename alloc_traits::allocator_type allocator; + typename bucket_alloc_traits::allocator_type bucket_allocator; bucket_alloc_pointer packed; std::size_t bucket; }; @@ -703,7 +699,7 @@ private: /*! @copydoc basic_storage */ template class basic_storage>>: public basic_sparse_set::template rebind_alloc> { - using underlying_type = basic_sparse_set; + using underlying_type = basic_sparse_set::template rebind_alloc>; public: /*! @brief Type of the objects assigned to entities. */ diff --git a/test/entt/entity/sparse_set.cpp b/test/entt/entity/sparse_set.cpp index 428a50be1..96c21a4b3 100644 --- a/test/entt/entity/sparse_set.cpp +++ b/test/entt/entity/sparse_set.cpp @@ -7,6 +7,8 @@ #include #include #include +#include "throwing_allocator.hpp" +#include "throwing_entity.hpp" struct empty_type {}; struct boxed_int { int value; }; @@ -580,3 +582,78 @@ TEST(SparseSet, CanModifyDuringIteration) { const auto entity = *it; (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{}; + + test::throwing_allocator::trigger_on_allocate = true; + + // strong exception safety + ASSERT_THROW(set.reserve(1u), 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; + + // 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); + ASSERT_TRUE(set.contains(entt::entity{0})); + + test::throwing_allocator::trigger_on_pointer_copy = 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); + ASSERT_TRUE(set.contains(entt::entity{0})); + + set.reserve(ENTT_PACKED_PAGE); + test::throwing_allocator::trigger_on_pointer_copy = true; + + // 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})); + + // 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 d3d2a810b..c84a126ae 100644 --- a/test/entt/entity/storage.cpp +++ b/test/entt/entity/storage.cpp @@ -7,6 +7,9 @@ #include #include #include +#include "throwing_allocator.hpp" +#include "throwing_component.hpp" +#include "throwing_entity.hpp" struct empty_type {}; struct boxed_int { int value; }; @@ -15,15 +18,6 @@ bool operator==(const boxed_int &lhs, const boxed_int &rhs) { return lhs.value == rhs.value; } -struct throwing_component { - struct constructor_exception: std::exception {}; - - [[noreturn]] throwing_component() { throw constructor_exception{}; } - - // necessary to disable the empty type optimization - int data; -}; - struct update_from_destructor { ~update_from_destructor() { if(target != entt::null) { @@ -760,18 +754,6 @@ TEST(Storage, MoveOnlyComponent) { (void)pool; } -TEST(Storage, EmplaceStrongExceptionGuarantee) { - entt::storage pool; - - try { - pool.emplace(entt::entity{0}); - } catch (const throwing_component::constructor_exception &) { - ASSERT_TRUE(pool.empty()); - } - - ASSERT_TRUE(pool.empty()); -} - TEST(Storage, UpdateFromDestructor) { static constexpr auto size = 10u; @@ -796,3 +778,118 @@ TEST(Storage, UpdateFromDestructor) { test(entt::entity(size - 1u)); 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; + + // strong exception safety + ASSERT_THROW(pool.emplace(entt::entity{0}, test::throwing_component{42}), typename test::throwing_component::exception_type); + ASSERT_TRUE(pool.empty()); + + const entt::entity entities[2u]{entt::entity{42}, entt::entity{1}}; + const test::throwing_component components[2u]{42, 1}; + + // basic exception safety + ASSERT_THROW(pool.insert(std::begin(entities), std::end(entities), test::throwing_component{42}), typename test::throwing_component::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_component::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_component::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); + + ASSERT_THROW(pool.erase(entt::entity{1}), typename test::throwing_component::exception_type); + ASSERT_FALSE(pool.empty()); + 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); +} + +TEST(Storage, ThrowingAllocator) { + entt::basic_storage> pool; + + test::throwing_allocator::trigger_on_allocate = true; + + // strong exception safety + ASSERT_THROW(pool.reserve(1u), test::throwing_allocator::exception_type); + ASSERT_EQ(pool.capacity(), 0u); + + test::throwing_allocator::trigger_after_allocate = true; + + // strong exception safety + 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; + + // 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()); +} diff --git a/test/entt/entity/throwing_allocator.hpp b/test/entt/entity/throwing_allocator.hpp new file mode 100644 index 000000000..eb5d35db4 --- /dev/null +++ b/test/entt/entity/throwing_allocator.hpp @@ -0,0 +1,170 @@ +#ifndef ENTT_ENTITY_THROWING_ALLOCATOR_HPP +#define ENTT_ENTITY_THROWING_ALLOCATOR_HPP + + +#include +#include +#include +#include + + +namespace test { + + +template +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 propagate_on_container_move_assignment = std::true_type; + using exception_type = test_exception; + + constexpr throwing_allocator() = default; + + template + throwing_allocator(const throwing_allocator &other) + : allocator{other.allocator} + {} + + pointer allocate(std::size_t length) { + if(trigger_on_allocate) { + trigger_on_allocate = false; + throw test_exception{}; + } + + trigger_on_allocate = trigger_after_allocate; + trigger_after_allocate = false; + + return allocator.allocate(length); + } + + void deallocate(pointer mem, std::size_t length) { + allocator.deallocate(mem.operator->(), length); + } + + static inline bool trigger_on_allocate{}; + static inline bool trigger_after_allocate{}; + static inline bool trigger_on_pointer_copy{}; + +private: + std::allocator allocator; +}; + + +} + + +#endif diff --git a/test/entt/entity/throwing_component.hpp b/test/entt/entity/throwing_component.hpp new file mode 100644 index 000000000..71b3ac217 --- /dev/null +++ b/test/entt/entity/throwing_component.hpp @@ -0,0 +1,49 @@ +#ifndef ENTT_ENTITY_THROWING_COMPONENT_HPP +#define ENTT_ENTITY_THROWING_COMPONENT_HPP + + +namespace test { + + +class throwing_component { + struct test_exception {}; + +public: + using exception_type = test_exception; + + throwing_component(int value) + : data{value} + {} + + throwing_component(const throwing_component &other) + : data{other.data} + { + if(data == trigger_on_value) { + throw exception_type{}; + } + } + + throwing_component & operator=(const throwing_component &other) { + if(other.data == trigger_on_value) { + throw exception_type{}; + } + + data = other.data; + return *this; + } + + operator int() const { + return data; + } + + static inline int trigger_on_value{}; + +private: + int data{}; +}; + + +} + + +#endif diff --git a/test/entt/entity/throwing_entity.hpp b/test/entt/entity/throwing_entity.hpp new file mode 100644 index 000000000..0ee834dfb --- /dev/null +++ b/test/entt/entity/throwing_entity.hpp @@ -0,0 +1,52 @@ +#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