Просмотр исходного кода

test: exception safety for sparse set and storage

Michele Caini 4 лет назад
Родитель
Сommit
b8bc3e4e94

+ 9 - 11
src/entt/entity/sparse_set.hpp

@@ -47,20 +47,18 @@ class basic_sparse_set {
 
     using traits_type = entt_traits<Entity>;
 
-    using alloc_type = typename std::allocator_traits<Allocator>::template rebind_alloc<Entity>;
-    using alloc_traits = std::allocator_traits<alloc_type>;
+    using alloc_traits = typename std::allocator_traits<Allocator>::template rebind_traits<Entity>;
     using alloc_pointer = typename alloc_traits::pointer;
     using alloc_const_pointer = typename alloc_traits::const_pointer;
 
-    using bucket_alloc_type = typename std::allocator_traits<Allocator>::template rebind_alloc<alloc_pointer>;
-    using bucket_alloc_traits = std::allocator_traits<bucket_alloc_type>;
+    using bucket_alloc_traits = typename std::allocator_traits<Allocator>::template rebind_traits<alloc_pointer>;
     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<Entity>;
+        friend class basic_sparse_set<Entity, Allocator>;
 
         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<typename traits_type::difference_type>(count)};
+        return iterator{std::addressof(packed), static_cast<typename traits_type::difference_type>(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;

+ 16 - 20
src/entt/entity/storage.hpp

@@ -53,29 +53,25 @@ class basic_storage: public basic_sparse_set<Entity, typename std::allocator_tra
 
     static constexpr auto packed_page = ENTT_PACKED_PAGE;
 
-    using underlying_type = basic_sparse_set<Entity>;
+    using underlying_type = basic_sparse_set<Entity, typename std::allocator_traits<Allocator>::template rebind_alloc<Entity>>;
     using traits_type = entt_traits<Entity>;
 
-    using alloc_type = typename std::allocator_traits<Allocator>::template rebind_alloc<Type>;
-    using alloc_traits = std::allocator_traits<alloc_type>;
+    using alloc_traits = typename std::allocator_traits<Allocator>::template rebind_traits<Type>;
     using alloc_pointer = typename alloc_traits::pointer;
     using alloc_const_pointer = typename alloc_traits::const_pointer;
 
-    using bucket_alloc_type = typename std::allocator_traits<Allocator>::template rebind_alloc<alloc_pointer>;
-    using bucket_alloc_traits = std::allocator_traits<bucket_alloc_type>;
+    using bucket_alloc_traits = typename std::allocator_traits<Allocator>::template rebind_traits<alloc_pointer>;
     using bucket_alloc_pointer = typename bucket_alloc_traits::pointer;
 
     using bucket_alloc_const_type = typename std::allocator_traits<Allocator>::template rebind_alloc<alloc_const_pointer>;
     using bucket_alloc_const_pointer = typename std::allocator_traits<bucket_alloc_const_type>::const_pointer;
 
-    using entity_alloc_type = typename std::allocator_traits<Allocator>::template rebind_alloc<Entity>;
-
     static_assert(alloc_traits::propagate_on_container_move_assignment::value);
     static_assert(bucket_alloc_traits::propagate_on_container_move_assignment::value);
 
     template<typename Value>
     class storage_iterator final {
-        friend class basic_storage<Entity, Type>;
+        friend class basic_storage<Entity, Type, Allocator>;
 
         storage_iterator(bucket_alloc_pointer const *ref, const typename traits_type::difference_type idx) ENTT_NOEXCEPT
             : packed{ref}, index{idx}
@@ -211,7 +207,7 @@ class basic_storage: public basic_sparse_set<Entity, typename std::allocator_tra
                     bucket_alloc_traits::deallocate(bucket_allocator, mem, length);
                     ENTT_THROW;
                 }
-            
+
                 for(auto pos = length; pos < bucket; ++pos) {
                     alloc_traits::deallocate(allocator, packed[pos], packed_page);
                 }
@@ -295,7 +291,7 @@ protected:
 
 public:
     /*! @brief Allocator type. */
-    using allocator_type = alloc_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. */
@@ -320,7 +316,7 @@ public:
      * @param alloc Allocator to use (possibly default-constructed).
      */
     explicit basic_storage(const allocator_type &alloc = {})
-        : basic_sparse_set<entity_type, entity_alloc_type>{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<entity_type, entity_alloc_type>{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<entity_type, entity_alloc_type>::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<typename Entity, typename Type, typename Allocator>
 class basic_storage<Entity, Type, Allocator, std::enable_if_t<is_empty_v<Type>>>: public basic_sparse_set<Entity, typename std::allocator_traits<Allocator>::template rebind_alloc<Entity>> {
-    using underlying_type = basic_sparse_set<Entity>;
+    using underlying_type = basic_sparse_set<Entity, typename std::allocator_traits<Allocator>::template rebind_alloc<Entity>>;
 
 public:
     /*! @brief Type of the objects assigned to entities. */

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

@@ -7,6 +7,8 @@
 #include <gtest/gtest.h>
 #include <entt/entity/sparse_set.hpp>
 #include <entt/entity/fwd.hpp>
+#include "throwing_allocator.hpp"
+#include "throwing_entity.hpp"
 
 struct empty_type {};
 struct boxed_int { int value; };
@@ -580,3 +582,78 @@ TEST(SparseSet, CanModifyDuringIteration) {
     const auto entity = *it;
     (void)entity;
 }
+
+TEST(SparseSet, ThrowingEntity) {
+    entt::basic_sparse_set<test::throwing_entity> set{};
+    test::throwing_entity::trigger_on_entity = 0u;
+
+    // strong exception safety
+    ASSERT_THROW(set.emplace(42), typename test::throwing_entity::exception_type);
+    ASSERT_TRUE(set.empty());
+
+    test::throwing_entity::trigger_on_entity = 42u;
+    const test::throwing_entity entities[2u]{42, 1};
+
+    // basic exception safety
+    ASSERT_THROW(set.insert(std::begin(entities), std::end(entities)), typename test::throwing_entity::exception_type);
+    ASSERT_EQ(set.size(), 0u);
+    ASSERT_FALSE(set.contains(1));
+
+    // basic exception safety
+    ASSERT_THROW(set.insert(std::rbegin(entities), std::rend(entities)), typename test::throwing_entity::exception_type);
+    ASSERT_EQ(set.size(), 1u);
+    ASSERT_TRUE(set.contains(1));
+}
+
+TEST(SparseSet, ThrowingAllocator) {
+    entt::basic_sparse_set<entt::entity, test::throwing_allocator<entt::entity>> set{};
+
+    test::throwing_allocator<entt::entity>::trigger_on_allocate = true;
+
+    // strong exception safety
+    ASSERT_THROW(set.reserve(1u), test::throwing_allocator<entt::entity>::exception_type);
+    ASSERT_EQ(set.capacity(), 0u);
+    ASSERT_EQ(set.extent(), 0u);
+
+    set.emplace(entt::entity{0});
+    test::throwing_allocator<entt::entity>::trigger_on_allocate = true;
+
+    // strong exception safety
+    ASSERT_THROW(set.reserve(2u), test::throwing_allocator<entt::entity>::exception_type);
+    ASSERT_EQ(set.capacity(), 1u);
+    ASSERT_EQ(set.extent(), ENTT_SPARSE_PAGE);
+    ASSERT_TRUE(set.contains(entt::entity{0}));
+
+    test::throwing_allocator<entt::entity>::trigger_on_pointer_copy = true;
+
+    // strong exception safety
+    ASSERT_THROW(set.reserve(2u), test::throwing_allocator<entt::entity>::exception_type);
+    ASSERT_EQ(set.capacity(), 1u);
+    ASSERT_EQ(set.extent(), ENTT_SPARSE_PAGE);
+    ASSERT_TRUE(set.contains(entt::entity{0}));
+
+    set.reserve(ENTT_PACKED_PAGE);
+    test::throwing_allocator<entt::entity>::trigger_on_pointer_copy = true;
+
+    // strong exception safety
+    ASSERT_THROW(set.emplace(entt::entity{ENTT_SPARSE_PAGE + 1u}), test::throwing_allocator<entt::entity>::exception_type);
+    ASSERT_EQ(set.capacity(), ENTT_PACKED_PAGE);
+    ASSERT_EQ(set.extent(), ENTT_SPARSE_PAGE);
+    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<entt::entity>::trigger_on_pointer_copy = true;
+
+    // strong exception safety
+    ASSERT_THROW(set.emplace(entt::entity{0}), test::throwing_allocator<entt::entity>::exception_type);
+    ASSERT_EQ(set.capacity(), ENTT_PACKED_PAGE);
+    ASSERT_EQ(set.extent(), 2 * ENTT_SPARSE_PAGE);
+    ASSERT_FALSE(set.contains(entt::entity{0}));
+    ASSERT_TRUE(set.contains(entt::entity{ENTT_SPARSE_PAGE + 1u}));
+}

+ 118 - 21
test/entt/entity/storage.cpp

@@ -7,6 +7,9 @@
 #include <gtest/gtest.h>
 #include <entt/entity/storage.hpp>
 #include <entt/entity/fwd.hpp>
+#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<throwing_component> 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<test::throwing_entity, int> pool;
+    test::throwing_entity::trigger_on_entity = 42u;
+
+    // strong exception safety
+    ASSERT_THROW(pool.emplace(42, 0), typename test::throwing_entity::exception_type);
+    ASSERT_TRUE(pool.empty());
+
+    const test::throwing_entity entities[2u]{42, 1};
+    const int components[2u]{42, 1};
+
+    // basic exception safety
+    ASSERT_THROW(pool.insert(std::begin(entities), std::end(entities), 1), typename test::throwing_entity::exception_type);
+    ASSERT_EQ(pool.size(), 0u);
+    ASSERT_FALSE(pool.contains(1));
+
+    // basic exception safety
+    ASSERT_THROW(pool.insert(std::rbegin(entities), std::rend(entities), 1), typename test::throwing_entity::exception_type);
+    ASSERT_EQ(pool.size(), 1u);
+    ASSERT_TRUE(pool.contains(1));
+    ASSERT_EQ(pool.get(1), 1);
+
+    pool.clear();
+
+    // basic exception safety
+    ASSERT_THROW(pool.insert(std::begin(entities), std::end(entities), std::begin(components)), typename test::throwing_entity::exception_type);
+    ASSERT_EQ(pool.size(), 0u);
+    ASSERT_FALSE(pool.contains(1));
+
+    // basic exception safety
+    ASSERT_THROW(pool.insert(std::rbegin(entities), std::rend(entities), std::rbegin(components)), typename test::throwing_entity::exception_type);
+    ASSERT_EQ(pool.size(), 1u);
+    ASSERT_TRUE(pool.contains(1));
+    ASSERT_EQ(pool.get(1), 1);
+}
+
+TEST(Storage, ThrowingComponent) {
+    entt::storage<test::throwing_component> pool;
+    test::throwing_component::trigger_on_value = 42;
+
+    // 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::entity>(entt::null));
+    // basice exception safety: no-leak guarantee, stored data contain valid values which may differ from the original values
+    ASSERT_EQ(pool.get(entt::entity{42}), 1);
+}
+
+TEST(Storage, ThrowingAllocator) {
+    entt::basic_storage<entt::entity, int, test::throwing_allocator<int>> pool;
+
+    test::throwing_allocator<int>::trigger_on_allocate = true;
+
+    // strong exception safety
+    ASSERT_THROW(pool.reserve(1u), test::throwing_allocator<int>::exception_type);
+    ASSERT_EQ(pool.capacity(), 0u);
+
+    test::throwing_allocator<int>::trigger_after_allocate = true;
+
+    // strong exception safety
+    ASSERT_THROW(pool.reserve(2 * ENTT_PACKED_PAGE), test::throwing_allocator<int>::exception_type);
+    ASSERT_EQ(pool.capacity(), 0u);
+
+    test::throwing_allocator<int>::trigger_on_pointer_copy = true;
+
+    // strong exception safety
+    ASSERT_THROW(pool.reserve(1u), test::throwing_allocator<int>::exception_type);
+    ASSERT_EQ(pool.capacity(), 0u);
+
+    pool.reserve(2 * ENTT_PACKED_PAGE);
+    test::throwing_allocator<test::throwing_allocator<int>::pointer>::trigger_on_pointer_copy = true;
+
+    // strong exception safety
+    ASSERT_THROW(pool.shrink_to_fit(), test::throwing_allocator<test::throwing_allocator<int>::pointer>::exception_type);
+    ASSERT_EQ(pool.capacity(), 2 * ENTT_PACKED_PAGE);
+
+    pool.shrink_to_fit();
+    test::throwing_allocator<int>::trigger_on_allocate = true;
+
+    // strong exception safety
+    ASSERT_THROW(pool.emplace(entt::entity{0}, 0), test::throwing_allocator<int>::exception_type);
+    ASSERT_FALSE(pool.contains(entt::entity{0}));
+    ASSERT_TRUE(pool.empty());
+}

+ 170 - 0
test/entt/entity/throwing_allocator.hpp

@@ -0,0 +1,170 @@
+#ifndef ENTT_ENTITY_THROWING_ALLOCATOR_HPP
+#define ENTT_ENTITY_THROWING_ALLOCATOR_HPP
+
+
+#include <cstddef>
+#include <iterator>
+#include <memory>
+#include <type_traits>
+
+
+namespace test {
+
+
+template<typename Type>
+class throwing_allocator {
+    template<typename Other>
+    friend class throwing_allocator;
+
+    struct fancy_pointer final {
+        using difference_type = typename std::iterator_traits<Type *>::difference_type;
+        using element_type = Type;
+        using value_type = element_type;
+        using pointer = value_type *;
+        using reference = value_type &;
+        using iterator_category = std::random_access_iterator_tag;
+
+        fancy_pointer(Type *init = nullptr)
+            : ptr{init}
+        {}
+
+        fancy_pointer(const fancy_pointer &other)
+            : ptr{other.ptr}
+        {
+            if(throwing_allocator::trigger_on_pointer_copy) {
+                throwing_allocator::trigger_on_pointer_copy = false;
+                throw test_exception{};
+            }
+        }
+
+        fancy_pointer & operator++() {
+            return ++ptr, *this;
+        }
+
+        fancy_pointer operator++(int) {
+            auto orig = *this;
+            return ++(*this), orig;
+        }
+
+        fancy_pointer & operator--() {
+            return --ptr, *this;
+        }
+
+        fancy_pointer operator--(int) {
+            auto orig = *this;
+            return operator--(), orig;
+        }
+
+        fancy_pointer & operator+=(const difference_type value) {
+            return (ptr += value, *this);
+        }
+
+        fancy_pointer operator+(const difference_type value) const {
+            auto copy = *this;
+            return (copy += value);
+        }
+
+        fancy_pointer & operator-=(const difference_type value) {
+            return (ptr -= value, *this);
+        }
+
+        fancy_pointer operator-(const difference_type value) const {
+            auto copy = *this;
+            return (copy -= value);
+        }
+
+        difference_type operator-(const fancy_pointer &other) const {
+            return ptr - other.ptr;
+        }
+
+        [[nodiscard]] reference operator[](const difference_type value) const {
+            return ptr[value];
+        }
+
+        [[nodiscard]] bool operator==(const fancy_pointer &other) const {
+            return other.ptr == ptr;
+        }
+
+        [[nodiscard]] bool operator!=(const fancy_pointer &other) const {
+            return !(*this == other);
+        }
+
+        [[nodiscard]] bool operator<(const fancy_pointer &other) const {
+            return ptr > other.ptr;
+        }
+
+        [[nodiscard]] bool operator>(const fancy_pointer &other) const {
+            return ptr < other.ptr;
+        }
+
+        [[nodiscard]] bool operator<=(const fancy_pointer &other) const {
+            return !(*this > other);
+        }
+
+        [[nodiscard]] bool operator>=(const fancy_pointer &other) const {
+            return !(*this < other);
+        }
+
+        explicit operator bool() const {
+            return (ptr != nullptr);
+        }
+
+        [[nodiscard]] pointer operator->() const {
+            return ptr;
+        }
+
+        [[nodiscard]] reference operator*() const {
+            return *ptr;
+        }
+
+    private:
+        Type *ptr;
+    };
+
+    struct test_exception {};
+
+public:
+    using value_type = Type;
+    using pointer = fancy_pointer;
+    using const_pointer = fancy_pointer;
+    using void_pointer = fancy_pointer;
+    using const_void_pointer = fancy_pointer;
+    using propagate_on_container_move_assignment = std::true_type;
+    using exception_type = test_exception;
+
+    constexpr throwing_allocator() = default;
+
+    template<class Other>
+    throwing_allocator(const throwing_allocator<Other> &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<Type> allocator;
+};
+
+
+}
+
+
+#endif

+ 49 - 0
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

+ 52 - 0
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