Quellcode durchsuchen

batch add is now available

Michele Caini vor 7 Jahren
Ursprung
Commit
9810da6982

+ 0 - 1
TODO

@@ -17,7 +17,6 @@
 * allow some features by component type (eg registry.assign(entity, component);
   - it could be possible for eg default constructible types by storing aside (pool data) erased functions
   - does it worth it?
-* add and bulk add with components (sort of registry.create<A, B>(first, last) and registry.create<A, B>())
 * events on replace, so that one can track updated components? indagate impact
   - define basic reactive systems (track entities to which component is attached, track entities from which component is removed, and so on)
   - define systems as composable mixins (initializazion, reactive, update, whatever) with flexible auto-detected arguments (registry, views, etc)

+ 28 - 6
docs/entity.md

@@ -6,9 +6,10 @@
 # Table of Contents
 
 * [Introduction](#introduction)
-* [Design choices](#design-choices)
+* [Design decisions](#design-decisions)
   * [A bitset-free entity-component system](#a-bitset-free-entity-component-system)
   * [Pay per use](#pay-per-use)
+  * [All or nothing](#all-or-nothing)
 * [Vademecum](#vademecum)
 * [The Registry, the Entity and the Component](#the-registry-the-entity-and-the-component)
   * [Observe changes](#observe-changes)
@@ -49,7 +50,7 @@ more) written in modern C++.<br/>
 The entity-component-system (also known as _ECS_) is an architectural pattern
 used mostly in game development.
 
-# Design choices
+# Design decisions
 
 ## A bitset-free entity-component system
 
@@ -90,6 +91,22 @@ performance along critical paths is high.
 So far, this choice has proven to be a good one and I really hope it can be for
 many others besides me.
 
+## All or nothing
+
+`EnTT` is such that at every moment a pair `(T *, size)` is available to
+directly access all the instances of a given component type `T`.<br/>
+This was a guideline and a design decision that influenced many choices, for
+better and for worse. I cannot say whether it will be useful or not to the
+reader, but it's worth to mention it, because it's of the corner stones of this
+library.
+
+Many of the tools described below, from the registry to the views and up to the
+groups give the possibility to get this information and have been designed
+around this need, which was and remains one of my main requirements during the
+development.<br/>
+The rest is experimentation and the desire to invent something new, hoping to
+have succeeded.
+
 # Vademecum
 
 The registry to store, the views and the groups to iterate. That's all.
@@ -129,7 +146,7 @@ Entities are represented by _entity identifiers_. An entity identifier is an
 opaque type that users should not inspect or modify in any way. It carries
 information about the entity itself and its version.
 
-A registry can be used both to construct and destroy entities:
+A registry can be used both to construct and to destroy entities:
 
 ```cpp
 // constructs a naked entity with no components and returns its identifier
@@ -139,9 +156,9 @@ auto entity = registry.create();
 registry.destroy(entity);
 ```
 
-There exist also overloads of the `create` and `destroy` member functions that
-accept two iterators, that is a range to assign or to destroy. It can be used to
-create or destroy multiple entities at once:
+There exists also an overload of the `create` and `destroy` member functions
+that accepts two iterators, that is a range to assign or to destroy. It can be
+used to create or destroy multiple entities at once:
 
 ```cpp
 // destroys all the entities in a range
@@ -149,6 +166,11 @@ auto view = registry.view<a_component, another_component>();
 registry.destroy(view.begin(), view.end());
 ```
 
+In both cases, the `create` member function accepts also a list of default
+constructible types of components to assign to the entities before to return.
+It's a faster alternative to the creation and subsequent assignment of
+components in separate steps.
+
 When an entity is destroyed, the registry can freely reuse it internally with a
 slightly different identifier. In particular, the version of an entity is
 increased each and every time it's discarded.<br/>

+ 93 - 24
src/entt/entity/registry.hpp

@@ -63,6 +63,9 @@ class registry {
     using signal_type = sigh<void(registry &, const Entity)>;
     using traits_type = entt_traits<Entity>;
 
+    template<typename Component>
+    using pool_type = sparse_set<Entity, std::decay_t<Component>>;
+
     template<typename, typename>
     struct non_owning_group;
 
@@ -136,6 +139,16 @@ class registry {
         std::size_t extent;
     };
 
+    void release(const Entity entity) {
+        // lengthens the implicit list of destroyed entities
+        const auto entt = entity & traits_type::entity_mask;
+        const auto version = ((entity >> traits_type::entity_shift) + 1) << traits_type::entity_shift;
+        const auto node = (available ? next : ((entt + 1) & traits_type::entity_mask)) | version;
+        entities[entt] = node;
+        next = entt;
+        ++available;
+    }
+
     template<typename Component>
     inline auto pool() const ENTT_NOEXCEPT {
         const auto ctype = type<Component>();
@@ -146,10 +159,10 @@ class registry {
             });
 
             assert(it != pools.cend() && it->pool);
-            return std::make_tuple(&*it, static_cast<sparse_set<Entity, std::decay_t<Component>> *>(it->pool.get()));
+            return std::make_tuple(&*it, static_cast<pool_type<Component> *>(it->pool.get()));
         } else {
             assert(ctype < pools.size() && pools[ctype].pool && pools[ctype].runtime_type == ctype);
-            return std::make_tuple(&pools[ctype], static_cast<sparse_set<Entity, std::decay_t<Component>> *>(pools[ctype].pool.get()));
+            return std::make_tuple(&pools[ctype], static_cast<pool_type<Component> *>(pools[ctype].pool.get()));
         }
     }
 
@@ -179,11 +192,11 @@ class registry {
         }
 
         if(!pdata->pool) {
-            pdata->pool = std::make_unique<sparse_set<Entity, std::decay_t<Component>>>();
+            pdata->pool = std::make_unique<pool_type<Component>>();
             pdata->runtime_type = ctype;
         }
 
-        return std::make_tuple(pdata, static_cast<sparse_set<Entity, std::decay_t<Component>> *>(pdata->pool.get()));
+        return std::make_tuple(pdata, static_cast<pool_type<Component> *>(pdata->pool.get()));
     }
 
 public:
@@ -328,7 +341,7 @@ public:
      * There are no guarantees on the order of the components. Use a view if you
      * want to iterate entities and components in the expected order.
      *
-     * @warning
+     * @note
      * Empty components aren't explicitly instantiated. Therefore, this function
      * always returns `nullptr` for them.
      *
@@ -452,11 +465,18 @@ public:
      * function can be used to know if they are still valid or the entity has
      * been destroyed and potentially recycled.
      *
-     * The returned entity has no components assigned.
+     * The returned entity has assigned the given components, if any. The
+     * components must be at least default constructible. A compilation error
+     * will occur otherwhise.
      *
-     * @return A valid entity identifier.
+     * @tparam Component Types of components to assign to the entity.
+     * @return A valid entity identifier if the component list is empty, a tuple
+     * containing the entity identifier and the references to the components
+     * just created otherwise.
      */
-    entity_type create() {
+    template<typename... Component>
+    std::conditional_t<sizeof...(Component) == 0, entity_type, std::tuple<entity_type, Component &...>>
+    create() {
         entity_type entity;
 
         if(available) {
@@ -472,7 +492,11 @@ public:
             assert(entity < traits_type::entity_mask);
         }
 
-        return entity;
+        if constexpr(sizeof...(Component) == 0) {
+            return entity;
+        } else {
+            return { entity, assign<Component>(entity)... };
+        }
     }
 
     /**
@@ -488,30 +512,64 @@ public:
      * function can be used to know if they are still valid or the entity has
      * been destroyed and potentially recycled.
      *
-     * The generated entities have no components assigned.
+     * The entities so generated have assigned the given components, if any. The
+     * components must be at least default constructible. A compilation error
+     * will occur otherwhise.
      *
+     * @tparam Component Types of components to assign to the entity.
      * @tparam It Type of forward iterator.
      * @param first An iterator to the first element of the range to generate.
      * @param last An iterator past the last element of the range to generate.
+     * @return No return value if the component list is empty, a tuple
+     * containing the pointers to the arrays of components just created and
+     * sorted the same of the entities otherwise.
      */
-    template<typename It>
-    void create(It first, It last) {
+    template<typename... Component, typename It>
+    std::conditional_t<sizeof...(Component) == 0, void, std::tuple<Component *...>>
+    create(It first, It last) {
         static_assert(std::is_convertible_v<entity_type, typename std::iterator_traits<It>::value_type>);
         const auto length = size_type(std::distance(first, last));
         const auto sz = std::min(available, length);
+        [[maybe_unused]] entity_type candidate{};
 
         available -= sz;
 
-        std::generate_n(first, sz, [this]() {
+        const auto tail = std::generate_n(first, sz, [&candidate, this]() mutable {
+            if constexpr(sizeof...(Component) > 0) {
+                candidate = std::max(candidate, next);
+            } else {
+                // suppress warnings
+                (void)candidate;
+            }
+
             const auto entt = next;
             const auto version = entities[entt] & (traits_type::version_mask << traits_type::entity_shift);
             next = entities[entt] & traits_type::entity_mask;
             return (entities[entt] = entt | version);
         });
 
-        std::generate_n((first + sz), (length - sz), [this]() {
+        std::generate(tail, last, [this]() {
             return entities.emplace_back(entity_type(entities.size()));
         });
+
+        if constexpr(sizeof...(Component) > 0) {
+            const auto hint = size_type(std::max(candidate, *(last-1)))+1;
+
+            auto generator = [first, last, hint, this](auto &&adata) {
+                auto *comp = std::get<1>(adata)->construct(first, last, hint);
+                auto *pdata = std::get<0>(adata);
+
+                if(!pdata->construction.empty()) {
+                    std::for_each(first, last, [pdata, this](const auto entity) {
+                        pdata->construction.publish(*this, entity);
+                    });
+                }
+
+                return comp;
+            };
+
+            return { generator(assure<Component>())... };
+        }
     }
 
     /**
@@ -550,14 +608,7 @@ public:
 
         // just a way to protect users from listeners that attach components
         assert(orphan(entity));
-
-        // lengthens the implicit list of destroyed entities
-        const auto entt = entity & traits_type::entity_mask;
-        const auto version = ((entity >> traits_type::entity_shift) + 1) << traits_type::entity_shift;
-        const auto node = (available ? next : ((entt + 1) & traits_type::entity_mask)) | version;
-        entities[entt] = node;
-        next = entt;
-        ++available;
+        release(entity);
     }
 
     /**
@@ -568,8 +619,26 @@ public:
      */
     template<typename It>
     void destroy(It first, It last) {
+        assert(std::all_of(first, last, [this](const auto entity) { return valid(entity); }));
+
+        for(auto pos = pools.size(); pos; --pos) {
+            auto &pdata = pools[pos-1];
+
+            if(pdata.pool) {
+                std::for_each(first, last, [&pdata, this](const auto entity) {
+                    if(pdata.pool->has(entity)) {
+                        pdata.destruction.publish(*this, entity);
+                        pdata.pool->destroy(entity);
+                    }
+                });
+            }
+        };
+
+        // just a way to protect users from listeners that attach components
+        assert(std::all_of(first, last, [this](const auto entity) { return orphan(entity); }));
+
         std::for_each(first, last, [this](const auto entity) {
-            destroy(entity);
+            release(entity);
         });
     }
 
@@ -1433,7 +1502,7 @@ public:
      * more instances of this class in sync, as an example in a client-server
      * architecture.
      *
-     * @warning
+     * @note
      * The loader returned by this function requires that the registry be empty.
      * In case it isn't, all the data will be automatically deleted before to
      * return.

+ 84 - 6
src/entt/entity/sparse_set.hpp

@@ -370,6 +370,41 @@ public:
         direct.push_back(entity);
     }
 
+    /**
+     * @brief Assigns one or more entities to a sparse set.
+     *
+     * This function requires to use a hint value for performance purposes.<br/>
+     * Its value indicates the size necessary to accommodate the largest entity
+     * if used as an index of a hypothetical array.
+     *
+     * @warning
+     * Attempting to assign an entity that already belongs to the sparse set
+     * results in undefined behavior.<br/>
+     * An assertion will abort the execution at runtime in debug mode if the
+     * sparse set already contains the given entity.
+     *
+     * @tparam It Type of forward 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.
+     * @param hint Hint value to avoid searching for the largest entity.
+     */
+    template<typename It>
+    void construct(It first, It last, size_type hint) {
+        if(hint > reverse.size()) {
+            // null is safe in all cases for our purposes
+            reverse.resize(hint, null);
+        }
+
+        std::for_each(first, last, [next = entity_type(direct.size()), this](const auto entity) mutable {
+            assert(!has(entity));
+            const auto pos = size_type(entity & traits_type::entity_mask);
+            assert(pos < reverse.size());
+            reverse[pos] = next++;
+        });
+
+        direct.insert(direct.end(), first, last);
+    }
+
     /**
      * @brief Removes an entity from a sparse set.
      *
@@ -750,7 +785,7 @@ public:
      * performance boost but less guarantees. Use `begin` and `end` if you want
      * to iterate the sparse set in the expected order.
      *
-     * @warning
+     * @note
      * Empty components aren't explicitly instantiated. Only one instance of the
      * given type is created. Therefore, this function always returns a pointer
      * to that instance.
@@ -873,7 +908,6 @@ public:
     /**
      * @brief Assigns an entity to a sparse set and constructs its object.
      *
-     * @note
      * This version accept both types that can be constructed in place directly
      * and types like aggregates that do not work well with a placement new as
      * performed usually under the hood during an _emplace back_.
@@ -907,6 +941,50 @@ public:
         }
     }
 
+    /**
+     * @brief Assigns one or more entities to a sparse set and constructs their
+     * objects.
+     *
+     * This function requires to use a hint value for performance purposes.<br/>
+     * Its value indicates the size necessary to accommodate the largest entity
+     * if used as an index of a hypothetical array.
+     *
+     * @note
+     * The object type must be at least default constructible.
+     *
+     * @note
+     * Empty components aren't explicitly instantiated. Only one instance of the
+     * given type is created. Therefore, this function always returns a pointer
+     * to that instance.
+     *
+     * @warning
+     * Attempting to assign an entity that already belongs to the sparse set
+     * results in undefined behavior.<br/>
+     * An assertion will abort the execution at runtime in debug mode if the
+     * sparse set already contains the given entity.
+     *
+     * @tparam It Type of forward 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.
+     * @param hint Hint value to avoid searching for the largest entity.
+     * @return A pointer to the array of instances just created and sorted the
+     * same of the entities.
+     */
+    template<typename It>
+    object_type * construct(It first, It last, const size_type hint) {
+        if constexpr(std::is_empty_v<object_type>) {
+            underlying_type::construct(first, last, hint);
+            return &instances;
+        } else {
+            static_assert(std::is_default_constructible_v<object_type>);
+            const auto offset = instances.size();
+            instances.insert(instances.end(), last-first, {});
+            // entity goes after component in case constructor throws
+            underlying_type::construct(first, last, hint);
+            return instances.data() + offset;
+        }
+    }
+
     /**
      * @brief Removes an entity from a sparse set and destroies its object.
      *
@@ -960,14 +1038,14 @@ public:
      * this member function.
      *
      * @note
+     * Empty components aren't explicitly instantiated. Therefore, this function
+     * isn't available for them.
+     *
+     * @note
      * Attempting to iterate elements using a raw pointer returned by a call to
      * either `data` or `raw` gives no guarantees on the order, even though
      * `sort` has been invoked.
      *
-     * @warning
-     * Empty components aren't explicitly instantiated. Therefore, this function
-     * isn't available for them.
-     *
      * @tparam Compare Type of comparison function object.
      * @tparam Sort Type of sort function object.
      * @tparam Args Types of arguments to forward to the sort function object.

+ 29 - 0
test/benchmark/benchmark.cpp

@@ -56,6 +56,35 @@ TEST(Benchmark, ConstructMany) {
     timer.elapsed();
 }
 
+TEST(Benchmark, ConstructManyAndAssignComponents) {
+    entt::registry<> registry;
+    std::vector<entt::registry<>::entity_type> entities(1000000);
+
+    std::cout << "Constructing 1000000 entities at once and assign components" << std::endl;
+
+    timer timer;
+
+    registry.create(entities.begin(), entities.end());
+
+    for(const auto entity: entities) {
+        registry.assign<position>(entity);
+        registry.assign<velocity>(entity);
+    }
+
+    timer.elapsed();
+}
+
+TEST(Benchmark, ConstructManyWithComponents) {
+    entt::registry<> registry;
+    std::vector<entt::registry<>::entity_type> entities(1000000);
+
+    std::cout << "Constructing 1000000 entities at once with components" << std::endl;
+
+    timer timer;
+    registry.create<position, velocity>(entities.begin(), entities.end());
+    timer.elapsed();
+}
+
 TEST(Benchmark, Destroy) {
     entt::registry<> registry;
 

+ 75 - 0
test/entt/entity/registry.cpp

@@ -938,6 +938,81 @@ TEST(Registry, CreateManyEntitiesAtOnce) {
     ASSERT_EQ(registry.version(entities[2]), entt::registry<>::version_type{0});
 }
 
+TEST(Registry, CreateAnEntityWithComponents) {
+    entt::registry<> registry;
+    const auto &[entity, ivalue, cvalue] = registry.create<int, char>();
+
+    ASSERT_FALSE(registry.empty<int>());
+    ASSERT_FALSE(registry.empty<char>());
+
+    ASSERT_EQ(registry.size<int>(), entt::registry<>::size_type{1});
+    ASSERT_EQ(registry.size<int>(), entt::registry<>::size_type{1});
+
+    ASSERT_TRUE((registry.has<int, char>(entity)));
+
+    ivalue = 42;
+    cvalue = 'c';
+
+    ASSERT_EQ(registry.get<int>(entity), 42);
+    ASSERT_EQ(registry.get<char>(entity), 'c');
+}
+
+TEST(Registry, CreateManyEntitiesWithComponentsAtOnce) {
+    entt::registry<> registry;
+    entt::registry<>::entity_type entities[3];
+
+    const auto entity = registry.create();
+    registry.destroy(registry.create());
+    registry.destroy(entity);
+    registry.destroy(registry.create());
+
+    const auto [iptr, cptr] = registry.create<int, char>(std::begin(entities), std::end(entities));
+
+    ASSERT_FALSE(registry.empty<int>());
+    ASSERT_FALSE(registry.empty<char>());
+
+    ASSERT_EQ(registry.size<int>(), entt::registry<>::size_type{3});
+    ASSERT_EQ(registry.size<int>(), entt::registry<>::size_type{3});
+
+    ASSERT_TRUE(registry.valid(entities[0]));
+    ASSERT_TRUE(registry.valid(entities[1]));
+    ASSERT_TRUE(registry.valid(entities[2]));
+
+    ASSERT_EQ(registry.entity(entities[0]), entt::registry<>::entity_type{0});
+    ASSERT_EQ(registry.version(entities[0]), entt::registry<>::version_type{2});
+
+    ASSERT_EQ(registry.entity(entities[1]), entt::registry<>::entity_type{1});
+    ASSERT_EQ(registry.version(entities[1]), entt::registry<>::version_type{1});
+
+    ASSERT_EQ(registry.entity(entities[2]), entt::registry<>::entity_type{2});
+    ASSERT_EQ(registry.version(entities[2]), entt::registry<>::version_type{0});
+
+    ASSERT_TRUE((registry.has<int, char>(entities[0])));
+    ASSERT_TRUE((registry.has<int, char>(entities[1])));
+    ASSERT_TRUE((registry.has<int, char>(entities[2])));
+
+    for(auto i = 0; i < 3; ++i) {
+        iptr[i] = i;
+        cptr[i] = char('a'+i);
+    }
+
+    for(auto i = 0; i < 3; ++i) {
+        ASSERT_EQ(registry.get<int>(entities[i]), i);
+        ASSERT_EQ(registry.get<char>(entities[i]), char('a'+i));
+    }
+}
+
+TEST(Registry, CreateManyEntitiesWithComponentsAtOnceWithListener) {
+    entt::registry<> registry;
+    entt::registry<>::entity_type entities[3];
+    listener listener;
+
+    registry.construction<int>().connect<&listener::incr<int>>(&listener);
+    registry.create<int, char>(std::begin(entities), std::end(entities));
+
+    ASSERT_EQ(listener.counter, 3);
+}
+
 TEST(Registry, NonOwningGroupInterleaved) {
     entt::registry<> registry;
     typename entt::registry<>::entity_type entity = entt::null;

+ 91 - 7
test/entt/entity/sparse_set.cpp

@@ -1,10 +1,13 @@
 #include <memory>
+#include <iterator>
 #include <exception>
 #include <algorithm>
 #include <unordered_set>
 #include <gtest/gtest.h>
 #include <entt/entity/sparse_set.hpp>
 
+struct empty_type {};
+
 TEST(SparseSetNoType, Functionalities) {
     entt::sparse_set<std::uint64_t> set;
 
@@ -58,6 +61,32 @@ TEST(SparseSetNoType, Functionalities) {
     other = std::move(set);
 }
 
+TEST(SparseSetNoType, ConstructMany) {
+    entt::sparse_set<std::uint64_t> set;
+    entt::sparse_set<std::uint64_t>::entity_type entities[2];
+
+    entities[0] = 3;
+    entities[1] = 42;
+
+    set.construct(12);
+    set.construct(std::begin(entities), std::end(entities), 43);
+    set.construct(24);
+
+    ASSERT_TRUE(set.has(entities[0]));
+    ASSERT_TRUE(set.has(entities[1]));
+    ASSERT_FALSE(set.has(0));
+    ASSERT_FALSE(set.has(9));
+    ASSERT_TRUE(set.has(12));
+    ASSERT_TRUE(set.has(24));
+
+    ASSERT_FALSE(set.empty());
+    ASSERT_EQ(set.size(), 4u);
+    ASSERT_EQ(set.get(12), 0u);
+    ASSERT_EQ(set.get(entities[0]), 1u);
+    ASSERT_EQ(set.get(entities[1]), 2u);
+    ASSERT_EQ(set.get(24), 3u);
+}
+
 TEST(SparseSetNoType, Iterator) {
     using iterator_type = typename entt::sparse_set<std::uint64_t>::iterator_type;
 
@@ -397,8 +426,7 @@ TEST(SparseSetWithType, Functionalities) {
     other = std::move(set);
 }
 
-TEST(SparseSetWithType, FunctionalitiesEmptyType) {
-    struct empty_type {};
+TEST(SparseSetWithType, EmptyType) {
     entt::sparse_set<std::uint64_t, empty_type> set;
 
     ASSERT_EQ(&set.construct(42), &set.construct(99));
@@ -407,6 +435,66 @@ TEST(SparseSetWithType, FunctionalitiesEmptyType) {
     ASSERT_EQ(std::as_const(set).try_get(42), std::as_const(set).try_get(99));
 }
 
+TEST(SparseSetWithType, ConstructMany) {
+    entt::sparse_set<std::uint64_t, int> set;
+    entt::sparse_set<std::uint64_t>::entity_type entities[2];
+
+    entities[0] = 3;
+    entities[1] = 42;
+
+    set.reserve(4);
+    set.construct(12, 21);
+    auto *component = set.construct(std::begin(entities), std::end(entities), 43);
+    set.construct(24, 42);
+
+    ASSERT_TRUE(set.has(entities[0]));
+    ASSERT_TRUE(set.has(entities[1]));
+    ASSERT_FALSE(set.has(0));
+    ASSERT_FALSE(set.has(9));
+    ASSERT_TRUE(set.has(12));
+    ASSERT_TRUE(set.has(24));
+
+    ASSERT_FALSE(set.empty());
+    ASSERT_EQ(set.size(), 4u);
+    ASSERT_EQ(set.get(12), 21);
+    ASSERT_EQ(set.get(entities[0]), 0);
+    ASSERT_EQ(set.get(entities[1]), 0);
+    ASSERT_EQ(set.get(24), 42);
+
+    component[0] = 1;
+    component[1] = 2;
+
+    ASSERT_EQ(set.get(entities[0]), 1);
+    ASSERT_EQ(set.get(entities[1]), 2);
+}
+
+TEST(SparseSetWithType, ConstructManyEmptyType) {
+    entt::sparse_set<std::uint64_t, empty_type> set;
+    entt::sparse_set<std::uint64_t>::entity_type entities[2];
+
+    entities[0] = 3;
+    entities[1] = 42;
+
+    set.reserve(4);
+    set.construct(12);
+    auto *component = set.construct(std::begin(entities), std::end(entities), 43);
+    set.construct(24);
+
+    ASSERT_TRUE(set.has(entities[0]));
+    ASSERT_TRUE(set.has(entities[1]));
+    ASSERT_FALSE(set.has(0));
+    ASSERT_FALSE(set.has(9));
+    ASSERT_TRUE(set.has(12));
+    ASSERT_TRUE(set.has(24));
+
+    ASSERT_FALSE(set.empty());
+    ASSERT_EQ(set.size(), 4u);
+    ASSERT_EQ(&set.get(entities[0]), &set.get(entities[1]));
+    ASSERT_EQ(&set.get(entities[0]), &set.get(12));
+    ASSERT_EQ(&set.get(entities[0]), &set.get(24));
+    ASSERT_EQ(&set.get(entities[0]), component);
+}
+
 TEST(SparseSetWithType, AggregatesMustWork) {
     struct aggregate_type { int value; };
     // the goal of this test is to enforce the requirements for aggregate types
@@ -509,7 +597,6 @@ TEST(SparseSetWithType, ConstIterator) {
 }
 
 TEST(SparseSetWithType, IteratorEmptyType) {
-    struct empty_type {};
     using iterator_type = typename entt::sparse_set<std::uint64_t, empty_type>::iterator_type;
     entt::sparse_set<std::uint64_t, empty_type> set;
     set.construct(3);
@@ -557,7 +644,6 @@ TEST(SparseSetWithType, IteratorEmptyType) {
 }
 
 TEST(SparseSetWithType, ConstIteratorEmptyType) {
-    struct empty_type {};
     using iterator_type = typename entt::sparse_set<std::uint64_t, empty_type>::const_iterator_type;
     entt::sparse_set<std::uint64_t, empty_type> set;
     set.construct(3);
@@ -621,7 +707,6 @@ TEST(SparseSetWithType, Raw) {
 }
 
 TEST(SparseSetWithType, RawEmptyType) {
-    struct empty_type {};
     entt::sparse_set<std::uint64_t, empty_type> set;
 
     set.construct(3);
@@ -933,7 +1018,6 @@ TEST(SparseSetWithType, RespectUnordered) {
 }
 
 TEST(SparseSetWithType, RespectOverlapEmptyType) {
-    struct empty_type {};
     entt::sparse_set<std::uint64_t, empty_type> lhs;
     entt::sparse_set<std::uint64_t, empty_type> rhs;
 
@@ -1050,7 +1134,7 @@ TEST(SparseSetWithType, ConstructorExceptionDoesNotAddToSet) {
     struct throwing_component {
         struct constructor_exception: std::exception {};
 
-        throwing_component() { throw constructor_exception{}; }
+        [[noreturn]] throwing_component() { throw constructor_exception{}; }
 
         // necessary to avoid the short-circuit construct() logic for empty objects
         int data;