Explorar el Código

entity:
* allocator support for sparse set and storage
* pointer stability when assigning components
* removed ::raw functions (registry, view, group)
* to_entity returns null for invalid components

Michele Caini hace 4 años
padre
commit
6194e12616

+ 49 - 55
docs/md/entity.md

@@ -7,10 +7,9 @@
 
 * [Introduction](#introduction)
 * [Design decisions](#design-decisions)
-  * [A bitset-free entity-component system](#a-bitset-free-entity-component-system)
+  * [Type-less and bitset-free](#type-less-and-bitset-free)
+  * [Build your own](#build-your-own)
   * [Pay per use](#pay-per-use)
-  * [All or nothing](#all-or-nothing)
-  * [Stateless systems](#stateless-systems)
 * [Vademecum](#vademecum)
 * [Pools](#pools)
 * [The Registry, the Entity and the Component](#the-registry-the-entity-and-the-component)
@@ -63,7 +62,7 @@ used mostly in game development.
 
 # Design decisions
 
-## A bitset-free entity-component system
+## Type-less and bitset-free
 
 `EnTT` offers a _bitset-free_ entity-component system that doesn't require users
 to specify the set of components neither at compile-time nor at runtime.<br/>
@@ -82,6 +81,23 @@ entt::registry<comp_0, comp_1, ..., comp_n> registry;
 Furthermore, it isn't necessary to announce the existence of a component type.
 When the time comes, just use it and that's all.
 
+## Build your own
+
+`EnTT` is designed as a container that can be used at any time just as a vector
+or any other tool would be used. It doesn't attempt in any way to take over on
+the user code base, nor to control its main loop or process scheduling.<br/>
+Unlike other more or less known models, it makes use of independent pools. This
+has some advantages and disadvantages. The main purpose is to provide a fully
+customizable tool, where users have the freedom to define pools and opaque
+proxies for types with specific requirements.
+
+The library provides a default implementation for many things and a mixin model
+that allows users to completely replace or even just enrich the pool dedicated
+to one or more components.<br/>
+The built-in signal support is an example of that: defined as a mixin, it's
+easily disabled if not needed. Similarly, poly storage is another example of how
+everything is customizable down to the smallest detail.
+
 ## Pay per use
 
 `EnTT` is entirely designed around the principle that users have to pay only for
@@ -102,28 +118,6 @@ performance where needed.
 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 since it's one of the corner stones of
-this library.
-
-Many of the tools described below give the possibility to get this information
-and have been designed around this need.<br/>
-The rest is experimentation and the desire to invent something new, hoping to
-have succeeded.
-
-## Stateless systems
-
-`EnTT` is designed so that it can work with _stateless systems_. In other words,
-all systems can be free functions and there is no need to define them as classes
-(although nothing prevents users from doing so).<br/>
-This is possible because the main class with which the users will work provides
-all what is needed to act as the sole _source of truth_ of an application.
-
 # Vademecum
 
 The registry to store, the views and the groups to iterate. That's all.
@@ -631,9 +625,7 @@ instance of a component and returns the entity associated with the latter:
 const auto entity = entt::to_entity(registry, position);
 ```
 
-This utility doesn't perform any check on the validity of the component.
-Therefore, trying to take the entity of an invalid element or of an instance
-that isn't associated with the given registry can result in undefined behavior.
+A null entity is returned in case the component doesn't belong to the registry.
 
 ### Dependencies
 
@@ -947,7 +939,7 @@ copying an entity will be as easy as:
 
 ```cpp
 registry.visit(entity, [&](const auto info) {
-    auto storage = registry.storage(info);
+    auto &&storage = registry.storage(info);
     storage->emplace(registry, other, storage->get(entity));
 });
 ```
@@ -957,8 +949,7 @@ Similarly, copying entire pools between different registries can look like this:
 
 ```cpp
 registry.visit([&](const auto info) {
-    auto storage = registry.storage(info);
-    other.storage(info)->insert(other, storage->data(), storage->raw(), storage->size());
+    registry.storage(info)->copy_to(other);
 });
 ```
 
@@ -1249,9 +1240,8 @@ data structures directly and avoid superfluous checks. There is nothing as fast
 as a single component view. In fact, they walk through a packed array of
 components and return them one at a time.<br/>
 Single component views offer a bunch of functionalities to get the number of
-entities they are going to return and a raw access to the entity list as well as
-to the component list. It's also possible to ask a view if it contains a given
-entity.<br/>
+entities they are going to return and a raw access to the entity list. It's also
+possible to ask a view if it contains a given entity.<br/>
 Refer to the inline documentation for all the details.
 
 Multi component views iterate entities that have at least all the given
@@ -1368,8 +1358,8 @@ the above type order rules apply sequentially.
 
 Runtime views iterate entities that have at least all the given components in
 their bags. During construction, these views look at the number of entities
-available for each component and pick up a reference to the smallest
-set of candidates in order to speed up iterations.<br/>
+available for each component and pick up a reference to the smallest set of
+candidates in order to speed up iterations.<br/>
 They offer more or less the same functionalities of a multi component view.
 However, they don't expose a `get` member function and users should refer to the
 registry that generated the view to access components. In particular, a runtime
@@ -1445,9 +1435,8 @@ However, it's unlikely that users will be able to appreciate the impact of
 groups on the other functionalities of a registry.
 
 Groups offer a bunch of functionalities to get the number of entities they are
-going to return and a raw access to the entity list as well as to the component
-list for owned components. It's also possible to ask a group if it contains a
-given entity.<br/>
+going to return and a raw access to the entity list. It's also possible to ask a
+group if it contains a given entity.<br/>
 Refer to the inline documentation for all the details.
 
 There is no need to store groups aside for they are extremely cheap to
@@ -1760,17 +1749,20 @@ not be used frequently to avoid the risk of a performance hit.
 ## What is allowed and what is not
 
 Most of the _ECS_ available out there don't allow to create and destroy entities
-and components during iterations.<br/>
+and components during iterations, nor to have pointer stability.<br/>
 `EnTT` partially solves the problem with a few limitations:
 
-* Creating entities and components is allowed during iterations in most cases.
+* Creating entities and components is allowed during iterations in most cases
+  and it never invalidates already existing references.
 
 * Deleting the current entity or removing its components is allowed during
-  iterations. For all the other entities, destroying them or removing their
-  components isn't allowed and can result in undefined behavior.
+  iterations but it could invalidate references. For all the other entities,
+  destroying them or removing their components isn't allowed and can result in
+  undefined behavior.
 
-In these cases, iterators aren't invalidated. To be clear, it doesn't mean that
-also references will continue to be valid.<br/>
+In other terms, iterators are never invalidated. Also, component references
+aren't invalidated when a new element is added while they could be invalidated
+upon deletion, due to the _swap-and-pop_ policy.<br/>
 Consider the following example:
 
 ```cpp
@@ -1780,13 +1772,15 @@ registry.view<position>([&](const auto entity, auto &pos) {
 });
 ```
 
-The `each` member function won't break (because iterators aren't invalidated)
-but there are no guarantees on references. Use a common range-for loop and get
-components directly from the view or move the creation of components at the end
-of the function to avoid dangling pointers.
+The `each` member function won't break (because iterators remain valid) nor will
+any reference be invalidated. Instead, more attention should be paid to the
+destruction of entities or the removal of components.<br/>
+Use a common range-for loop and get components directly from the view or move
+the deletion of entities and components at the end of the function to avoid
+dangling pointers.
 
-Iterators are invalidated instead and the behavior is undefined if an entity is
-modified or destroyed and it's not the one currently returned by the iterator
+Conversely, iterators are invalidated and the behavior is undefined if an entity
+is modified or destroyed and it's not the one currently returned by the iterator
 nor a newly created one.<br/>
 To work around it, possible approaches are:
 
@@ -1852,9 +1846,9 @@ When an empty type is detected, it's not instantiated in any case. Therefore,
 only the entities to which it's assigned are made available.<br/>
 There doesn't exist a way to _get_ empty types from a registry, views and groups
 will never return instances for them (for example, during a call to `each`) and
-some functions such as `try_get` or the raw access to the list of components
-won't be available. Finally, the `sort` functionality will onlyaccepts callbacks
-that require to return entities rather than components:
+some functions such as `try_get` aren't available for empty types. Finally, the
+`sort` functionality will only accepts callbacks that require to return entities
+rather than components:
 
 ```cpp
 registry.sort<empty_type>([](const entt::entity lhs, const entt::entity rhs) {

+ 3 - 2
src/entt/entity/fwd.hpp

@@ -2,17 +2,18 @@
 #define ENTT_ENTITY_FWD_HPP
 
 
+#include <memory>
 #include "../core/fwd.hpp"
 
 
 namespace entt {
 
 
-template<typename>
+template<typename Entity, typename = std::allocator<Entity>>
 class basic_sparse_set;
 
 
-template<typename, typename, typename>
+template<typename, typename Type, typename = std::allocator<Type>, typename = void>
 class basic_storage;
 
 

+ 2 - 22
src/entt/entity/group.hpp

@@ -680,26 +680,6 @@ public:
         return !*this || !*length;
     }
 
-    /**
-     * @brief Direct access to the raw representation offered by the storage.
-     *
-     * For fully contiguous storage classes, the returned pointer is such that
-     * range `[raw<Component>(), raw<Component>() + size())` is always a valid
-     * range, even if the container is empty.
-     *
-     * @warning
-     * This function is only available for owned types.
-     *
-     * @tparam Component Type of component in which one is interested.
-     * @return A pointer to the array of components.
-     */
-    template<typename Component>
-    [[nodiscard]] auto raw() const ENTT_NOEXCEPT {
-        static_assert((std::is_same_v<Component, Owned> || ...), "Non-owned type");
-        auto *cpool = std::get<storage_type<Component> *>(pools);
-        return cpool ? cpool->raw() : decltype(cpool->raw()){};
-    }
-
     /**
      * @brief Direct access to the list of entities.
      *
@@ -940,7 +920,7 @@ public:
     template<typename... Component, typename Compare, typename Sort = std_sort, typename... Args>
     void sort(Compare compare, Sort algo = Sort{}, Args &&... args) const {
         auto *cpool = std::get<0>(pools);
-        
+
         if constexpr(sizeof...(Component) == 0) {
             static_assert(std::is_invocable_v<Compare, const entity_type, const entity_type>, "Invalid comparison function");
             cpool->sort_n(*length, std::move(compare), std::move(algo), std::forward<Args>(args)...);
@@ -953,7 +933,7 @@ public:
                 return compare(std::forward_as_tuple(std::get<storage_type<Component> *>(pools)->get(lhs)...), std::forward_as_tuple(std::get<storage_type<Component> *>(pools)->get(rhs)...));
             }, std::move(algo), std::forward<Args>(args)...);
         }
-        
+
         [this](auto *head, auto *... other) {
             for(auto next = *length; next; --next) {
                 const auto pos = next - 1;

+ 16 - 3
src/entt/entity/helper.hpp

@@ -135,16 +135,29 @@ void invoke(basic_registry<Entity> &reg, const Entity entt) {
 
 /**
  * @brief Returns the entity associated with a given component.
+ *
+ * @warning
+ * Currently, this function only works correctly with the default pool as it
+ * makes assumptions about how the components are laid out.
+ *
  * @tparam Entity A valid entity type (see entt_traits for more details).
  * @tparam Component Type of component.
  * @param reg A registry that contains the given entity and its components.
- * @param component A valid component instance.
+ * @param instance A valid component instance.
  * @return The entity associated with the given component.
  */
 template<typename Entity, typename Component>
-Entity to_entity(const basic_registry<Entity> &reg, const Component &component) {
+Entity to_entity(const basic_registry<Entity> &reg, const Component &instance) {
     const auto view = reg.template view<const Component>();
-    return *(view.data() + (&component - view.raw()));
+    const auto *addr = std::addressof(instance);
+
+    for(auto it = view.rbegin(), last = view.rend(); it < last; it += ENTT_PAGE_SIZE) {
+        if(const auto dist = (addr - std::addressof(view.template get<const Component>(*it))); dist >= 0 && dist < ENTT_PAGE_SIZE) {
+            return *(it + dist);
+        }
+    }
+
+    return entt::null;
 }
 
 

+ 203 - 99
src/entt/entity/sparse_set.hpp

@@ -2,12 +2,11 @@
 #define ENTT_ENTITY_SPARSE_SET_HPP
 
 
+#include <cstddef>
 #include <iterator>
-#include <utility>
-#include <vector>
 #include <memory>
-#include <cstddef>
 #include <type_traits>
+#include <utility>
 #include "../config/config.h"
 #include "../core/algorithm.hpp"
 #include "entity.hpp"
@@ -38,22 +37,34 @@ namespace entt {
  * a sparse set. Do not make assumption on the order in any case.
  *
  * @tparam Entity A valid entity type (see entt_traits for more details).
+ * @tparam Allocator Type of allocator used to manage memory and elements.
  */
-template<typename Entity>
+template<typename Entity, typename Allocator>
 class basic_sparse_set {
+    static constexpr auto growth_factor = 1.5;
     static constexpr auto page_size = ENTT_PAGE_SIZE;
 
     using traits_type = entt_traits<Entity>;
-    using page_type = std::unique_ptr<Entity[]>;
+
+    using alloc_type = typename std::allocator_traits<Allocator>::template rebind_alloc<Entity>;
+    using alloc_traits = std::allocator_traits<alloc_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_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>;
 
-        using packed_type = std::vector<Entity>;
         using index_type = typename traits_type::difference_type;
 
-        sparse_set_iterator(const packed_type &ref, const index_type idx) ENTT_NOEXCEPT
-            : packed{&ref}, index{idx}
+        sparse_set_iterator(const alloc_const_pointer *ref, const index_type idx) ENTT_NOEXCEPT
+            : packed{ref}, index{idx}
         {}
 
     public:
@@ -144,32 +155,107 @@ class basic_sparse_set {
         }
 
     private:
-        const packed_type *packed;
+        const alloc_const_pointer *packed;
         index_type index;
     };
 
-    [[nodiscard]] auto page(const Entity entt) const ENTT_NOEXCEPT {
+    [[nodiscard]] static auto page(const Entity entt) ENTT_NOEXCEPT {
         return size_type{(to_integral(entt) & traits_type::entity_mask) / page_size};
     }
 
-    [[nodiscard]] auto offset(const Entity entt) const ENTT_NOEXCEPT {
+    [[nodiscard]] static auto offset(const Entity entt) ENTT_NOEXCEPT {
         return size_type{to_integral(entt) & (page_size - 1)};
     }
 
-    [[nodiscard]] page_type & assure(const std::size_t pos) {
-        if(!(pos < sparse.size())) {
-            sparse.resize(pos+1);
+    [[nodiscard]] auto assure_page(const std::size_t idx) {
+        if(!(idx < bucket)) {
+            const size_type sz = idx + 1u;
+            const auto old = std::exchange(sparse, bucket_alloc_traits::allocate(bucket_allocator, sz));
+            std::uninitialized_fill(sparse + bucket, sparse + sz, alloc_pointer{});
+
+            if(const auto curr = std::exchange(bucket, sz); curr) {
+                for(size_type pos{}; pos < curr; ++pos) {
+                    bucket_alloc_traits::construct(bucket_allocator, std::addressof(sparse[pos]), std::move(old[pos]));
+                    bucket_alloc_traits::destroy(bucket_allocator, std::addressof(old[pos]));
+                }
+
+                bucket_alloc_traits::deallocate(bucket_allocator, old, curr);
+            }
         }
 
-        if(!sparse[pos]) {
-            sparse[pos].reset(new entity_type[page_size]);
-            // null is safe in all cases for our purposes
-            for(auto *first = sparse[pos].get(), *last = first + page_size; first != last; ++first) {
-                *first = null;
+        if(!sparse[idx]) {
+            sparse[idx] = alloc_traits::allocate(allocator, page_size);
+            std::uninitialized_fill(sparse[idx], sparse[idx] + page_size, null);
+        }
+
+        return sparse[idx];
+    }
+
+    void resize_packed(const std::size_t req) {
+        ENTT_ASSERT(req && !(req < count), "Invalid request");
+        auto old = std::exchange(packed, alloc_traits::allocate(allocator, req));
+
+        if(const auto length = std::exchange(reserved, req); length) {
+            for(size_type pos{}; pos < count; ++pos) {
+                alloc_traits::construct(allocator, std::addressof(packed[pos]), std::move(old[pos]));
+                alloc_traits::destroy(allocator, std::addressof(old[pos]));
             }
+
+            alloc_traits::deallocate(allocator, old, length);
+        }
+    }
+
+    template<typename It>
+    void push_back(It first, It last) {
+        if(const std::size_t req = count + std::distance(first, last); reserved < req) {
+            const std::size_t sz = reserved * growth_factor + !reserved;
+            resize_packed(sz < req ? req : sz);
         }
 
-        return sparse[pos];
+        for(; first != last; ++first) {
+            ENTT_ASSERT(!contains(*first), "Set already contains entity");
+            assure_page(page(*first))[offset(*first)] = entity_type{static_cast<typename traits_type::entity_type>(count)};
+            alloc_traits::construct(allocator, std::addressof(packed[count++]), *first);
+        }
+    }
+
+    void pop(const Entity entt, void *ud) {
+        ENTT_ASSERT(contains(entt), "Set does not contain entity");
+        // last chance to use the entity for derived classes and mixins, if any
+        about_to_erase(entt, ud);
+
+        auto &ref = sparse[page(entt)][offset(entt)];
+        const auto pos = size_type{to_integral(ref)};
+
+        const auto last = --count;
+        packed[pos] = std::exchange(packed[last], entt);
+        sparse[page(packed[pos])][offset(packed[pos])] = ref;
+        // no risks when pos == count, accessing packed is no longer required
+        alloc_traits::destroy(allocator, std::addressof(packed[last]));
+        ref = null;
+
+        // don't expect exceptions here, instead allow for nosy destructors
+        swap_and_pop(pos);
+    }
+
+    void reset_to_empty() {
+        if(const auto length = std::exchange(reserved, 0u); length) {
+            std::destroy(packed, packed + std::exchange(count, 0u));
+            alloc_traits::deallocate(allocator, packed, length);
+        }
+
+        if(const auto length = std::exchange(bucket, 0u); length) {
+            for(size_type pos{}; pos < length; ++pos) {
+                if(sparse[pos]) {
+                    std::destroy(sparse[pos], sparse[pos] + page_size);
+                    alloc_traits::deallocate(allocator, sparse[pos], page_size);
+                }
+
+                bucket_alloc_traits::destroy(bucket_allocator, std::addressof(sparse[pos]));
+            }
+
+            bucket_alloc_traits::deallocate(bucket_allocator, sparse, std::exchange(bucket, 0u));
+        }
     }
 
 protected:
@@ -194,26 +280,67 @@ protected:
     virtual void about_to_erase([[maybe_unused]] const Entity entity, [[maybe_unused]] void *ud) {}
 
 public:
+    /*! @brief Allocator type. */
+    using allocator_type = alloc_type;
     /*! @brief Underlying entity identifier. */
     using entity_type = Entity;
     /*! @brief Unsigned integer type. */
     using size_type = std::size_t;
+    /*! @brief Pointer type to contained entities. */
+    using pointer = alloc_const_pointer;
     /*! @brief Random access iterator type. */
     using iterator = sparse_set_iterator;
     /*! @brief Reverse iterator type. */
-    using reverse_iterator = const entity_type *;
+    using reverse_iterator = pointer;
 
-    /*! @brief Default constructor. */
-    basic_sparse_set() = default;
+    /**
+     * @brief Default constructor.
+     * @param alloc Allocator to use (possibly default-constructed).
+     */
+    explicit basic_sparse_set(const allocator_type &alloc = {})
+        : allocator{alloc},
+          bucket_allocator{alloc},
+          sparse{},
+          packed{},
+          bucket{},
+          count{},
+          reserved{}
+    {}
 
-    /*! @brief Default move constructor. */
-    basic_sparse_set(basic_sparse_set &&) = default;
+    /**
+     * @brief Move constructor.
+     * @param other The instance to move from.
+     */
+    basic_sparse_set(basic_sparse_set &&other) ENTT_NOEXCEPT
+        : allocator{std::move(other.allocator)},
+          bucket_allocator{std::move(other.bucket_allocator)},
+          sparse{std::exchange(other.sparse, bucket_alloc_pointer{})},
+          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)}
+    {}
 
     /*! @brief Default destructor. */
-    virtual ~basic_sparse_set() = default;
+    virtual ~basic_sparse_set() {
+        reset_to_empty();
+    }
 
-    /*! @brief Default move assignment operator. @return This sparse set. */
-    basic_sparse_set & operator=(basic_sparse_set &&) = default;
+    /**
+     * @brief Move assignment operator.
+     * @param other The instance to move from.
+     * @return This sparse set.
+     */
+    basic_sparse_set & operator=(basic_sparse_set &&other) ENTT_NOEXCEPT {
+        allocator = std::move(other.allocator);
+        bucket_allocator = std::move(other.bucket_allocator);
+        sparse = std::exchange(other.sparse, bucket_alloc_pointer{});
+        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);
+        return *this;
+    }
 
     /**
      * @brief Increases the capacity of a sparse set.
@@ -224,7 +351,9 @@ public:
      * @param cap Desired capacity.
      */
     void reserve(const size_type cap) {
-        packed.reserve(cap);
+        if(cap > reserved) {
+            resize_packed(cap);
+        }
     }
 
     /**
@@ -233,18 +362,16 @@ public:
      * @return Capacity of the sparse set.
      */
     [[nodiscard]] size_type capacity() const ENTT_NOEXCEPT {
-        return packed.capacity();
+        return reserved;
     }
 
     /*! @brief Requests the removal of unused capacity. */
     void shrink_to_fit() {
-        // conservative approach
-        if(packed.empty()) {
-            sparse.clear();
+        if(!count) {
+            reset_to_empty();
+        } else if(reserved > count) {
+            resize_packed(count);
         }
-
-        sparse.shrink_to_fit();
-        packed.shrink_to_fit();
     }
 
     /**
@@ -258,7 +385,7 @@ public:
      * @return Extent of the sparse set.
      */
     [[nodiscard]] size_type extent() const ENTT_NOEXCEPT {
-        return sparse.size() * page_size;
+        return bucket * page_size;
     }
 
     /**
@@ -272,7 +399,7 @@ public:
      * @return Number of elements.
      */
     [[nodiscard]] size_type size() const ENTT_NOEXCEPT {
-        return packed.size();
+        return count;
     }
 
     /**
@@ -280,23 +407,15 @@ public:
      * @return True if the sparse set is empty, false otherwise.
      */
     [[nodiscard]] bool empty() const ENTT_NOEXCEPT {
-        return packed.empty();
+        return (count == size_type{});
     }
 
     /**
      * @brief Direct access to the internal packed array.
-     *
-     * The returned pointer is such that range `[data(), data() + size())` is
-     * always a valid range, even if the container is empty.
-     *
-     * @note
-     * Entities are in the reverse order as returned by the `begin`/`end`
-     * iterators.
-     *
      * @return A pointer to the internal packed array.
      */
-    [[nodiscard]] const entity_type * data() const ENTT_NOEXCEPT {
-        return packed.data();
+    [[nodiscard]] pointer data() const ENTT_NOEXCEPT {
+        return packed;
     }
 
     /**
@@ -309,8 +428,7 @@ public:
      * @return An iterator to the first entity of the internal packed array.
      */
     [[nodiscard]] iterator begin() const ENTT_NOEXCEPT {
-        const typename traits_type::difference_type pos = packed.size();
-        return iterator{packed, pos};
+        return iterator{&packed, static_cast<typename traits_type::difference_type>(count)};
     }
 
     /**
@@ -324,7 +442,7 @@ public:
      * internal packed array.
      */
     [[nodiscard]] iterator end() const ENTT_NOEXCEPT {
-        return iterator{packed, {}};
+        return iterator{&packed, {}};
     }
 
     /**
@@ -338,7 +456,7 @@ public:
      * array.
      */
     [[nodiscard]] reverse_iterator rbegin() const ENTT_NOEXCEPT {
-        return packed.data();
+        return data();
     }
 
     /**
@@ -352,7 +470,7 @@ public:
      * reversed internal packed array.
      */
     [[nodiscard]] reverse_iterator rend() const ENTT_NOEXCEPT {
-        return rbegin() + packed.size();
+        return rbegin() + count;
     }
 
     /**
@@ -373,7 +491,7 @@ public:
     [[nodiscard]] bool contains(const entity_type entt) const {
         const auto curr = page(entt);
         // testing against null permits to avoid accessing the packed array
-        return (curr < sparse.size() && sparse[curr] && sparse[curr][offset(entt)] != null);
+        return (curr < bucket && sparse[curr] && sparse[curr][offset(entt)] != null);
     }
 
     /**
@@ -397,7 +515,7 @@ public:
      * @return The entity at specified location if any, a null entity otherwise.
      */
     [[nodiscard]] entity_type at(const size_type pos) const {
-        return pos < packed.size() ? packed[pos] : null;
+        return pos < count ? packed[pos] : null;
     }
 
     /**
@@ -406,7 +524,7 @@ public:
      * @return The entity at specified location.
      */
     [[nodiscard]] entity_type operator[](const size_type pos) const {
-        ENTT_ASSERT(pos < packed.size(), "Position is out of bounds");
+        ENTT_ASSERT(pos < count, "Position is out of bounds");
         return packed[pos];
     }
 
@@ -420,9 +538,8 @@ public:
      * @param entt A valid entity identifier.
      */
     void emplace(const entity_type entt) {
-        ENTT_ASSERT(!contains(entt), "Set already contains entity");
-        assure(page(entt))[offset(entt)] = entity_type{static_cast<typename traits_type::entity_type>(packed.size())};
-        packed.push_back(entt);
+        entity_type arr[1u]{entt};
+        push_back(arr, arr + 1u);
     }
 
     /**
@@ -438,13 +555,7 @@ public:
      */
     template<typename It>
     void insert(It first, It last) {
-        auto next = static_cast<typename traits_type::entity_type>(packed.size());
-        packed.insert(packed.end(), first, last);
-
-        for(; first != last; ++first) {
-            ENTT_ASSERT(!contains(*first), "Set already contains entity");
-            assure(page(*first))[offset(*first)] = entity_type{next++};
-        }
+        push_back(first, last);
     }
 
     /**
@@ -458,24 +569,7 @@ public:
      * @param ud Optional user data that are forwarded as-is to derived classes.
      */
     void erase(const entity_type entt, void *ud = nullptr) {
-        ENTT_ASSERT(contains(entt), "Set does not contain entity");
-
-        // last chance to use the entity for derived classes and mixins, if any
-        about_to_erase(entt, ud);
-
-        auto &ref = sparse[page(entt)][offset(entt)];
-        const auto pos = size_type{to_integral(ref)};
-
-        const auto other = packed.back();
-        sparse[page(other)][offset(other)] = ref;
-        ref = null;
-
-        // if it looks weird, imagine what the subtle bugs it prevents are
-        ENTT_ASSERT((packed.back() = entt, true), "");
-        packed[pos] = other;
-        packed.pop_back();
-
-        swap_and_pop(pos);
+        pop(entt, ud);
     }
 
     /**
@@ -488,7 +582,7 @@ public:
     template<typename It>
     void erase(It first, It last, void *ud = nullptr) {
         for(; first != last; ++first) {
-            erase(*first, ud);
+            pop(*first, ud);
         }
     }
 
@@ -499,7 +593,7 @@ public:
      * @return True if the entity is actually removed, false otherwise.
      */
     bool remove(const entity_type entt, void *ud = nullptr) {
-        return contains(entt) ? (erase(entt, ud), true) : false;
+        return contains(entt) ? (pop(entt, ud), true) : false;
     }
 
     /**
@@ -512,13 +606,13 @@ public:
      */
     template<typename It>
     size_type remove(It first, It last, void *ud = nullptr) {
-        size_type count{};
+        size_type found{};
 
         for(; first != last; ++first) {
-            count += remove(*first, ud);
+            found += remove(*first, ud);
         }
 
-        return count;
+        return found;
     }
 
     /**
@@ -537,9 +631,12 @@ public:
     void swap(const entity_type lhs, const entity_type rhs) {
         const auto from = index(lhs);
         const auto to = index(rhs);
+
+        // derived classes first for a bare-minimum exception safety guarantee
+        swap_at(from, to);
+
         std::swap(sparse[page(lhs)][offset(lhs)], sparse[page(rhs)][offset(rhs)]);
         std::swap(packed[from], packed[to]);
-        swap_at(from, to);
     }
 
     /**
@@ -567,16 +664,18 @@ public:
      * @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.
-     * @param count Number of elements to sort.
+     * @param length Number of elements to sort.
      * @param compare A valid comparison function object.
      * @param algo A valid sort function object.
      * @param args Arguments to forward to the sort function object, if any.
      */
     template<typename Compare, typename Sort = std_sort, typename... Args>
-    void sort_n(const size_type count, Compare compare, Sort algo = Sort{}, Args &&... args) {
-        algo(packed.rend() - count, packed.rend(), std::move(compare), std::forward<Args>(args)...);
+    void sort_n(const size_type length, Compare compare, Sort algo = Sort{}, Args &&... args) {
+        ENTT_ASSERT(!(length > count), "Length exceeds the number of elements");
+
+        algo(std::make_reverse_iterator(packed + length), std::make_reverse_iterator(packed), std::move(compare), std::forward<Args>(args)...);
 
-        for(size_type pos{}, last = (size() < count ? size() : count); pos < last; ++pos) {
+        for(size_type pos{}; pos < length; ++pos) {
             auto curr = pos;
             auto next = index(packed[curr]);
 
@@ -607,7 +706,7 @@ public:
      */
     template<typename Compare, typename Sort = std_sort, typename... Args>
     void sort(Compare compare, Sort algo = Sort{}, Args &&... args) {
-        sort_n(size(), std::move(compare), std::move(algo), std::forward<Args>(args)...);
+        sort_n(count, std::move(compare), std::move(algo), std::forward<Args>(args)...);
     }
 
     /**
@@ -629,7 +728,7 @@ public:
         const auto to = other.end();
         auto from = other.begin();
 
-        size_type pos = packed.size() - 1;
+        size_type pos = count - 1;
 
         while(pos && from != to) {
             if(contains(*from)) {
@@ -653,8 +752,13 @@ public:
     }
 
 private:
-    std::vector<page_type> sparse;
-    std::vector<entity_type> packed;
+    alloc_type allocator;
+    bucket_alloc_type bucket_allocator;
+    bucket_alloc_pointer sparse;
+    alloc_pointer packed;
+    std::size_t bucket;
+    std::size_t count;
+    std::size_t reserved;
 };
 
 

+ 225 - 81
src/entt/entity/storage.hpp

@@ -2,13 +2,12 @@
 #define ENTT_ENTITY_STORAGE_HPP
 
 
-#include <algorithm>
 #include <cstddef>
 #include <iterator>
+#include <memory>
 #include <tuple>
 #include <type_traits>
 #include <utility>
-#include <vector>
 #include "../config/config.h"
 #include "../core/algorithm.hpp"
 #include "../core/type_traits.hpp"
@@ -30,9 +29,7 @@ namespace entt {
  * to the entities.
  *
  * @note
- * Entities and objects have the same order. It's guaranteed both in case of raw
- * access (either to entities or objects) and when using random or input access
- * iterators.
+ * Entities and objects have the same order.
  *
  * @note
  * Internal data structures arrange elements to maximize performance. There are
@@ -47,27 +44,40 @@ namespace entt {
  *
  * @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<typename Entity, typename Type, typename = void>
-class basic_storage: public basic_sparse_set<Entity> {
+template<typename Entity, typename Type, typename Allocator, typename>
+class basic_storage: public basic_sparse_set<Entity, typename std::allocator_traits<Allocator>::template rebind_alloc<Entity>> {
     static_assert(std::is_move_constructible_v<Type> && std::is_move_assignable_v<Type>, "The managed type must be at least move constructible and assignable");
 
+    static constexpr auto growth_factor = 1.5;
+    static constexpr auto page_size = ENTT_PAGE_SIZE;
+
     using underlying_type = basic_sparse_set<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_pointer = typename alloc_traits::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_pointer = typename bucket_alloc_traits::pointer;
+    using bucket_alloc_const_pointer = typename bucket_alloc_traits::const_pointer;
+
+    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>;
 
-        using instance_type = constness_as_t<std::vector<Type>, Value>;
-        using index_type = typename traits_type::difference_type;
-
-        storage_iterator(instance_type &ref, const index_type idx) ENTT_NOEXCEPT
-            : instances{&ref}, index{idx}
+        storage_iterator(const bucket_alloc_const_pointer *ref, const typename traits_type::difference_type idx) ENTT_NOEXCEPT
+            : packed{ref}, index{idx}
         {}
 
     public:
-        using difference_type = index_type;
+        using difference_type = typename traits_type::difference_type;
         using value_type = Value;
         using pointer = value_type *;
         using reference = value_type &;
@@ -117,7 +127,7 @@ class basic_storage: public basic_sparse_set<Entity> {
 
         [[nodiscard]] reference operator[](const difference_type value) const ENTT_NOEXCEPT {
             const auto pos = size_type(index-value-1);
-            return (*instances)[pos];
+            return (*packed)[page(pos)][offset(pos)];
         }
 
         [[nodiscard]] bool operator==(const storage_iterator &other) const ENTT_NOEXCEPT {
@@ -146,7 +156,7 @@ class basic_storage: public basic_sparse_set<Entity> {
 
         [[nodiscard]] pointer operator->() const ENTT_NOEXCEPT {
             const auto pos = size_type(index-1u);
-            return &(*instances)[pos];
+            return &(*packed)[page(pos)][offset(pos)];
         }
 
         [[nodiscard]] reference operator*() const ENTT_NOEXCEPT {
@@ -154,25 +164,104 @@ class basic_storage: public basic_sparse_set<Entity> {
         }
 
     private:
-        instance_type *instances;
-        index_type index;
+        const bucket_alloc_const_pointer *packed;
+        difference_type index;
     };
 
+    [[nodiscard]] static auto page(const std::size_t pos) ENTT_NOEXCEPT {
+        return pos / page_size;
+    }
+
+    [[nodiscard]] static auto offset(const std::size_t pos) ENTT_NOEXCEPT {
+        return pos & (page_size - 1);
+    }
+
+    void maybe_resize_packed(const std::size_t req) {
+        ENTT_ASSERT(req && !(req < count), "Invalid request");
+        if(const auto length = std::exchange(bucket, (req + page_size - 1u) / page_size); bucket != length) {
+            const auto old = std::exchange(packed, bucket_alloc_traits::allocate(bucket_allocator, bucket));
+
+            if(bucket > length) {
+                if(length) {
+                    for(size_type pos{}; pos < length; ++pos) {
+                        bucket_alloc_traits::construct(bucket_allocator, std::addressof(packed[pos]), old[pos]);
+                        bucket_alloc_traits::destroy(bucket_allocator, std::addressof(old[pos]));
+                    }
+
+                    bucket_alloc_traits::deallocate(bucket_allocator, old, length);
+                }
+
+                for(auto pos = length; pos < bucket; ++pos) {
+                    bucket_alloc_traits::construct(bucket_allocator, std::addressof(packed[pos]), alloc_traits::allocate(allocator, page_size));
+                }
+            } else if(bucket < length) {
+                for(size_type pos{}; pos < bucket; ++pos) {
+                    bucket_alloc_traits::construct(bucket_allocator, std::addressof(packed[pos]), old[pos]);
+                    bucket_alloc_traits::destroy(bucket_allocator, std::addressof(old[pos]));
+                }
+
+                for(auto pos = bucket; pos < length; ++pos) {
+                    alloc_traits::deallocate(allocator, old[pos], page_size);
+                    bucket_alloc_traits::destroy(bucket_allocator, std::addressof(old[pos]));
+                }
+
+                bucket_alloc_traits::deallocate(bucket_allocator, old, length);
+            }
+        }
+    }
+
+    template<typename... Args>
+    Type & push_back(Args &&... args) {
+        ENTT_ASSERT(count != (bucket * page_size), "No more space left");
+
+        if constexpr(std::is_aggregate_v<value_type>) {
+            alloc_traits::construct(allocator, std::addressof(*(packed[page(count)] + offset(count))), Type{std::forward<Args>(args)...});
+        } else {
+            alloc_traits::construct(allocator, std::addressof(*(packed[page(count)] + offset(count))), std::forward<Args>(args)...);
+        }
+
+        const auto last = count++;
+        return packed[page(last)][offset(last)];
+    }
+
+    void reset_to_empty() {
+        if(const auto length = std::exchange(bucket, 0u); length) {
+            for(size_type pos{}; pos < length; ++pos) {
+                if(count) {
+                    const auto sz = count > page_size ? page_size : count;
+                    std::destroy(packed[pos], packed[pos] + sz);
+                    count -= sz;
+                }
+
+                alloc_traits::deallocate(allocator, packed[pos], page_size);
+                bucket_alloc_traits::destroy(bucket_allocator, std::addressof(packed[pos]));
+            }
+
+            bucket_alloc_traits::deallocate(bucket_allocator, packed, length);
+        }
+    }
+
 protected:
     /*! @copydoc basic_sparse_set::swap_at */
     void swap_at(const std::size_t lhs, const std::size_t rhs) final {
-        std::swap(instances[lhs], instances[rhs]);
+        std::swap(packed[page(lhs)][offset(lhs)], packed[page(rhs)][offset(rhs)]);
     }
 
     /*! @copydoc basic_sparse_set::swap_and_pop */
     void swap_and_pop(const std::size_t pos) final {
-        // required because pop_back isn't guaranteed to shrink before removing the element
-        [[maybe_unused]] auto other = std::move(instances[pos]);
-        instances[pos] = std::move(instances.back());
-        instances.pop_back();
+        auto &&elem = packed[page(pos)][offset(pos)];
+        [[maybe_unused]] auto other = std::move(elem);
+
+        auto &&last = packed[page(count - 1)][offset(count - 1)];
+        elem = std::move(last);
+        alloc_traits::destroy(allocator, std::addressof(last));
+
+        --count;
     }
 
 public:
+    /*! @brief Allocator type. */
+    using allocator_type = alloc_type;
     /*! @brief Type of the objects assigned to entities. */
     using value_type = Type;
     /*! @brief Underlying entity identifier. */
@@ -184,9 +273,52 @@ public:
     /*! @brief Constant random access iterator type. */
     using const_iterator = storage_iterator<const Type>;
     /*! @brief Reverse iterator type. */
-    using reverse_iterator = Type *;
+    using reverse_iterator = std::reverse_iterator<iterator>;
     /*! @brief Constant reverse iterator type. */
-    using const_reverse_iterator = const Type *;
+    using const_reverse_iterator = std::reverse_iterator<const_iterator>;
+
+    /**
+     * @brief Default constructor.
+     * @param alloc Allocator to use (possibly default-constructed).
+     */
+    explicit basic_storage(const allocator_type &alloc = {})
+        : allocator{alloc},
+          bucket_allocator{alloc},
+          packed{},
+          bucket{},
+          count{}
+    {}
+
+    /**
+     * @brief Move constructor.
+     * @param other The instance to move from.
+     */
+    basic_storage(basic_storage &&other) ENTT_NOEXCEPT
+        : allocator{std::move(other.allocator)},
+          bucket_allocator{std::move(other.bucket_allocator)},
+          packed{std::exchange(other.packed, bucket_alloc_pointer{})},
+          bucket{std::exchange(other.bucket, 0u)},
+          count{std::exchange(other.count, 0u)}
+    {}
+
+    /*! @brief Default destructor. */
+    ~basic_storage() override {
+        reset_to_empty();
+    }
+
+    /**
+     * @brief Move assignment operator.
+     * @param other The instance to move from.
+     * @return This sparse set.
+     */
+    basic_storage & operator=(basic_storage &&other) ENTT_NOEXCEPT {
+        allocator = std::move(other.allocator);
+        bucket_allocator = std::move(other.bucket_allocator);
+        packed = std::exchange(other.packed, bucket_alloc_pointer{});
+        bucket = std::exchange(other.bucket, 0u);
+        count = std::exchange(other.count, 0u);
+        return *this;
+    }
 
     /**
      * @brief Increases the capacity of a storage.
@@ -198,34 +330,25 @@ public:
      */
     void reserve(const size_type cap) {
         underlying_type::reserve(cap);
-        instances.reserve(cap);
-    }
 
-    /*! @brief Requests the removal of unused capacity. */
-    void shrink_to_fit() {
-        underlying_type::shrink_to_fit();
-        instances.shrink_to_fit();
+        if(cap > (bucket * page_size)) {
+            maybe_resize_packed(cap);
+        }
     }
 
     /**
-     * @brief Direct access to the array of objects.
-     *
-     * The returned pointer is such that range `[raw(), raw() + size())` is
-     * always a valid range, even if the container is empty.
-     *
-     * @note
-     * Objects are in the reverse order as returned by the `begin`/`end`
-     * iterators.
-     *
-     * @return A pointer to the array of objects.
+     * @brief Returns the number of elements that a storage has currently
+     * allocated space for.
+     * @return Capacity of the storage.
      */
-    [[nodiscard]] const value_type * raw() const ENTT_NOEXCEPT {
-        return instances.data();
+    [[nodiscard]] size_type capacity() const ENTT_NOEXCEPT {
+        return bucket * page_size;
     }
 
-    /*! @copydoc raw */
-    [[nodiscard]] value_type * raw() ENTT_NOEXCEPT {
-        return const_cast<value_type *>(std::as_const(*this).raw());
+    /*! @brief Requests the removal of unused capacity. */
+    void shrink_to_fit() {
+        underlying_type::shrink_to_fit();
+        count ? maybe_resize_packed(count) : reset_to_empty();
     }
 
     /**
@@ -238,7 +361,7 @@ public:
      */
     [[nodiscard]] const_iterator cbegin() const ENTT_NOEXCEPT {
         const typename traits_type::difference_type pos = underlying_type::size();
-        return const_iterator{instances, pos};
+        return const_iterator{&packed, pos};
     }
 
     /*! @copydoc cbegin */
@@ -249,7 +372,7 @@ public:
     /*! @copydoc begin */
     [[nodiscard]] iterator begin() ENTT_NOEXCEPT {
         const typename traits_type::difference_type pos = underlying_type::size();
-        return iterator{instances, pos};
+        return iterator{&packed, pos};
     }
 
     /**
@@ -263,7 +386,7 @@ public:
      * internal array.
      */
     [[nodiscard]] const_iterator cend() const ENTT_NOEXCEPT {
-        return const_iterator{instances, {}};
+        return const_iterator{&packed, {}};
     }
 
     /*! @copydoc cend */
@@ -273,7 +396,7 @@ public:
 
     /*! @copydoc end */
     [[nodiscard]] iterator end() ENTT_NOEXCEPT {
-        return iterator{instances, {}};
+        return iterator{&packed, {}};
     }
 
     /**
@@ -286,7 +409,7 @@ public:
      * @return An iterator to the first instance of the reversed internal array.
      */
     [[nodiscard]] const_reverse_iterator crbegin() const ENTT_NOEXCEPT {
-        return instances.data();
+        return std::make_reverse_iterator(cend());
     }
 
     /*! @copydoc crbegin */
@@ -296,7 +419,7 @@ public:
 
     /*! @copydoc rbegin */
     [[nodiscard]] reverse_iterator rbegin() ENTT_NOEXCEPT {
-        return instances.data();
+        return std::make_reverse_iterator(end());
     }
 
     /**
@@ -310,7 +433,7 @@ public:
      * reversed internal array.
      */
     [[nodiscard]] const_reverse_iterator crend() const ENTT_NOEXCEPT {
-        return crbegin() + instances.size();
+        return std::make_reverse_iterator(cbegin());
     }
 
     /*! @copydoc crend */
@@ -320,7 +443,7 @@ public:
 
     /*! @copydoc rend */
     [[nodiscard]] reverse_iterator rend() ENTT_NOEXCEPT {
-        return rbegin() + instances.size();
+        return std::make_reverse_iterator(begin());
     }
 
     /**
@@ -334,7 +457,8 @@ public:
      * @return The object assigned to the entity.
      */
     [[nodiscard]] const value_type & get(const entity_type entt) const {
-        return instances[underlying_type::index(entt)];
+        const auto idx = underlying_type::index(entt);
+        return packed[page(idx)][offset(idx)];
     }
 
     /*! @copydoc get */
@@ -360,15 +484,11 @@ public:
      */
     template<typename... Args>
     value_type & emplace(const entity_type entt, Args &&... args) {
-        if constexpr(std::is_aggregate_v<value_type>) {
-            instances.push_back(Type{std::forward<Args>(args)...});
-        } else {
-            instances.emplace_back(std::forward<Args>(args)...);
-        }
-
+        maybe_resize_packed(count + 1u);
+        auto &value = push_back(std::forward<Args>(args)...);
         // entity goes after component in case constructor throws
         underlying_type::emplace(entt);
-        return instances.back();
+        return value;
     }
 
     /**
@@ -380,9 +500,10 @@ public:
      */
     template<typename... Func>
     decltype(auto) patch(const entity_type entity, Func &&... func) {
-        auto &&instance = instances[this->index(entity)];
-        (std::forward<Func>(func)(instance), ...);
-        return instance;
+        const auto idx = underlying_type::index(entity);
+        auto &&elem = packed[page(idx)][offset(idx)];
+        (std::forward<Func>(func)(elem), ...);
+        return elem;
     }
 
     /**
@@ -400,9 +521,15 @@ public:
      */
     template<typename It>
     void insert(It first, It last, const value_type &value = {}) {
-        instances.insert(instances.end(), std::distance(first, last), value);
-        // entities go after components in case constructors throw
-        underlying_type::insert(first, last);
+        if(const auto length = std::distance(first, last); length) {
+            maybe_resize_packed(count + length);
+
+            for(auto pos = length; pos; --pos) {
+                push_back(value);
+            }
+
+            underlying_type::insert(first, last);
+        }
     }
 
     /**
@@ -420,9 +547,15 @@ public:
      */
     template<typename EIt, typename CIt>
     void insert(EIt first, EIt last, CIt from, CIt to) {
-        instances.insert(instances.end(), from, to);
-        // entities go after components in case constructors throw
-        underlying_type::insert(first, last);
+        if(const auto length = std::distance(from, to); length) {
+            maybe_resize_packed(count + length);
+
+            for(; from != to; ++from) {
+                push_back(*from);
+            }
+
+            underlying_type::insert(first, last);
+        }
     }
 
     /**
@@ -455,19 +588,20 @@ public:
      * @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.
-     * @param count Number of elements to sort.
+     * @param length Number of elements to sort.
      * @param compare A valid comparison function object.
      * @param algo A valid sort function object.
      * @param args Arguments to forward to the sort function object, if any.
      */
     template<typename Compare, typename Sort = std_sort, typename... Args>
-    void sort_n(const size_type count, Compare compare, Sort algo = Sort{}, Args &&... args) {
+    void sort_n(const size_type length, Compare compare, Sort algo = Sort{}, Args &&... args) {
         if constexpr(std::is_invocable_v<Compare, const value_type &, const value_type &>) {
-            underlying_type::sort_n(count, [this, compare = std::move(compare)](const auto lhs, const auto rhs) {
-                return compare(std::as_const(instances[underlying_type::index(lhs)]), std::as_const(instances[underlying_type::index(rhs)]));
+            underlying_type::sort_n(length, [this, compare = std::move(compare)](const auto lhs, const auto rhs) {
+                const auto ilhs = underlying_type::index(lhs), irhs = underlying_type::index(rhs);
+                return compare(std::as_const(packed[page(ilhs)][offset(ilhs)]), std::as_const(packed[page(irhs)][offset(irhs)]));
             }, std::move(algo), std::forward<Args>(args)...);
         } else {
-            underlying_type::sort_n(count, std::move(compare), std::move(algo), std::forward<Args>(args)...);
+            underlying_type::sort_n(length, std::move(compare), std::move(algo), std::forward<Args>(args)...);
         }
     }
 
@@ -485,17 +619,21 @@ public:
      */
     template<typename Compare, typename Sort = std_sort, typename... Args>
     void sort(Compare compare, Sort algo = Sort{}, Args &&... args) {
-        sort_n(this->size(), std::move(compare), std::move(algo), std::forward<Args>(args)...);
+        sort_n(underlying_type::size(), std::move(compare), std::move(algo), std::forward<Args>(args)...);
     }
 
 private:
-    std::vector<value_type> instances;
+    alloc_type allocator;
+    bucket_alloc_type bucket_allocator;
+    bucket_alloc_pointer packed;
+    std::size_t bucket;
+    std::size_t count;
 };
 
 
 /*! @copydoc basic_storage */
-template<typename Entity, typename Type>
-class basic_storage<Entity, Type, std::enable_if_t<is_empty_v<Type>>>: public basic_sparse_set<Entity> {
+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>;
 
 public:
@@ -516,7 +654,7 @@ public:
      * @param entt A valid entity identifier.
      */
     void get([[maybe_unused]] const entity_type entt) const {
-        ENTT_ASSERT(this->contains(entt), "Storage does not contain entity");
+        ENTT_ASSERT(underlying_type::contains(entt), "Storage does not contain entity");
     }
 
     /**
@@ -544,7 +682,7 @@ public:
     */
     template<typename... Func>
     void patch([[maybe_unused]] const entity_type entity, Func &&... func) {
-        ENTT_ASSERT(this->contains(entity), "Storage does not contain entity");
+        ENTT_ASSERT(underlying_type::contains(entity), "Storage does not contain entity");
         (std::forward<Func>(func)(), ...);
     }
 
@@ -579,6 +717,9 @@ struct storage_adapter_mixin: Type {
     /*! @brief Underlying entity identifier. */
     using entity_type = typename Type::entity_type;
 
+    /*! @brief Inherited constructors. */
+    using Type::Type;
+
     /**
      * @brief Assigns entities to a storage.
      * @tparam Args Types of arguments to use to construct the object.
@@ -639,6 +780,9 @@ public:
     /*! @brief Underlying entity identifier. */
     using entity_type = typename Type::entity_type;
 
+    /*! @brief Inherited constructors. */
+    using Type::Type;
+
     /**
      * @brief Returns a sink object.
      *

+ 0 - 13
src/entt/entity/view.hpp

@@ -708,19 +708,6 @@ public:
         return std::get<0>(pools)->empty();
     }
 
-    /**
-     * @brief Direct access to the raw representation offered by the storage.
-     *
-     * For fully contiguous storage classes, the returned pointer is such that
-     * range `[raw<Component>(), raw<Component>() + size())` is always a valid
-     * range, even if the container is empty.
-     *
-     * @return A pointer to the array of components.
-     */
-    [[nodiscard]] auto raw() const ENTT_NOEXCEPT {
-        return std::get<0>(pools)->raw();
-    }
-
     /**
      * @brief Direct access to the list of entities.
      *

+ 29 - 44
test/entt/entity/group.cpp

@@ -665,8 +665,8 @@ TEST(OwningGroup, Functionalities) {
 
     ASSERT_EQ(group.size(), 1u);
 
-    ASSERT_EQ(*(cgroup.raw<const int>() + 0), 42);
-    ASSERT_EQ(*(group.raw<int>() + 0), 42);
+    ASSERT_EQ(*(cgroup.rbegin() + 0), e1);
+    ASSERT_EQ(*(group.rbegin() + 0), e1);
 
     for(auto entity: group) {
         ASSERT_EQ(std::get<0>(cgroup.get<const int, const char>(entity)), 42);
@@ -675,7 +675,6 @@ TEST(OwningGroup, Functionalities) {
     }
 
     ASSERT_EQ(*(group.data() + 0), e1);
-    ASSERT_EQ(*(group.raw<int>() + 0), 42);
 
     registry.erase<char>(e0);
     registry.erase<char>(e1);
@@ -706,7 +705,6 @@ TEST(OwningGroup, Invalid) {
     ASSERT_TRUE(group.empty());
     ASSERT_EQ(group.size(), 0u);
 
-    ASSERT_EQ(group.raw<const int>(), nullptr);
     ASSERT_EQ(group.data(), nullptr);
 
     ASSERT_EQ(group.begin(), group.end());
@@ -844,18 +842,14 @@ TEST(OwningGroup, SortOrdered) {
     ASSERT_EQ(*(group.data() + 0u), entities[0]);
     ASSERT_EQ(*(group.data() + 1u), entities[1]);
     ASSERT_EQ(*(group.data() + 2u), entities[2]);
-    ASSERT_EQ(*(group.data() + 3u), entities[3]);
-    ASSERT_EQ(*(group.data() + 4u), entities[4]);
 
-    ASSERT_EQ((group.raw<boxed_int>() + 0u)->value, 12);
-    ASSERT_EQ((group.raw<boxed_int>() + 1u)->value, 9);
-    ASSERT_EQ((group.raw<boxed_int>() + 2u)->value, 6);
-    ASSERT_EQ((group.raw<boxed_int>() + 3u)->value, 1);
-    ASSERT_EQ((group.raw<boxed_int>() + 4u)->value, 2);
+    ASSERT_EQ(group.get<boxed_int>(*(group.data() + 0u)).value, 12);
+    ASSERT_EQ(group.get<boxed_int>(*(group.data() + 1u)).value, 9);
+    ASSERT_EQ(group.get<boxed_int>(*(group.data() + 2u)).value, 6);
 
-    ASSERT_EQ(*(group.raw<char>() + 0u), 'a');
-    ASSERT_EQ(*(group.raw<char>() + 1u), 'b');
-    ASSERT_EQ(*(group.raw<char>() + 2u), 'c');
+    ASSERT_EQ(group.get<char>(*(group.data() + 0u)), 'a');
+    ASSERT_EQ(group.get<char>(*(group.data() + 1u)), 'b');
+    ASSERT_EQ(group.get<char>(*(group.data() + 2u)), 'c');
 
     ASSERT_EQ((group.get<boxed_int, char>(entities[0])), (std::make_tuple(boxed_int{12}, 'a')));
     ASSERT_EQ((group.get<boxed_int, char>(entities[1])), (std::make_tuple(boxed_int{9}, 'b')));
@@ -891,18 +885,14 @@ TEST(OwningGroup, SortReverse) {
     ASSERT_EQ(*(group.data() + 0u), entities[2]);
     ASSERT_EQ(*(group.data() + 1u), entities[1]);
     ASSERT_EQ(*(group.data() + 2u), entities[0]);
-    ASSERT_EQ(*(group.data() + 3u), entities[3]);
-    ASSERT_EQ(*(group.data() + 4u), entities[4]);
 
-    ASSERT_EQ((group.raw<boxed_int>() + 0u)->value, 12);
-    ASSERT_EQ((group.raw<boxed_int>() + 1u)->value, 9);
-    ASSERT_EQ((group.raw<boxed_int>() + 2u)->value, 6);
-    ASSERT_EQ((group.raw<boxed_int>() + 3u)->value, 1);
-    ASSERT_EQ((group.raw<boxed_int>() + 4u)->value, 2);
+    ASSERT_EQ(group.get<boxed_int>(*(group.data() + 0u)).value, 12);
+    ASSERT_EQ(group.get<boxed_int>(*(group.data() + 1u)).value, 9);
+    ASSERT_EQ(group.get<boxed_int>(*(group.data() + 2u)).value, 6);
 
-    ASSERT_EQ(*(group.raw<char>() + 0u), 'c');
-    ASSERT_EQ(*(group.raw<char>() + 1u), 'b');
-    ASSERT_EQ(*(group.raw<char>() + 2u), 'a');
+    ASSERT_EQ(group.get<char>(*(group.data() + 0u)), 'c');
+    ASSERT_EQ(group.get<char>(*(group.data() + 1u)), 'b');
+    ASSERT_EQ(group.get<char>(*(group.data() + 2u)), 'a');
 
     ASSERT_EQ((group.get<boxed_int, char>(entities[0])), (std::make_tuple(boxed_int{6}, 'a')));
     ASSERT_EQ((group.get<boxed_int, char>(entities[1])), (std::make_tuple(boxed_int{9}, 'b')));
@@ -948,16 +938,18 @@ TEST(OwningGroup, SortUnordered) {
     ASSERT_EQ(*(group.data() + 2u), entities[0]);
     ASSERT_EQ(*(group.data() + 3u), entities[1]);
     ASSERT_EQ(*(group.data() + 4u), entities[2]);
-    ASSERT_EQ(*(group.data() + 5u), entities[5]);
-    ASSERT_EQ(*(group.data() + 6u), entities[6]);
 
-    ASSERT_EQ((group.raw<boxed_int>() + 0u)->value, 12);
-    ASSERT_EQ((group.raw<boxed_int>() + 1u)->value, 9);
-    ASSERT_EQ((group.raw<boxed_int>() + 2u)->value, 6);
-    ASSERT_EQ((group.raw<boxed_int>() + 3u)->value, 3);
-    ASSERT_EQ((group.raw<boxed_int>() + 4u)->value, 1);
-    ASSERT_EQ((group.raw<boxed_int>() + 5u)->value, 4);
-    ASSERT_EQ((group.raw<boxed_int>() + 6u)->value, 5);
+    ASSERT_EQ(group.get<boxed_int>(*(group.data() + 0u)).value, 12);
+    ASSERT_EQ(group.get<boxed_int>(*(group.data() + 1u)).value, 9);
+    ASSERT_EQ(group.get<boxed_int>(*(group.data() + 2u)).value, 6);
+    ASSERT_EQ(group.get<boxed_int>(*(group.data() + 3u)).value, 3);
+    ASSERT_EQ(group.get<boxed_int>(*(group.data() + 4u)).value, 1);
+
+    ASSERT_EQ(group.get<char>(*(group.data() + 0u)), 'e');
+    ASSERT_EQ(group.get<char>(*(group.data() + 1u)), 'd');
+    ASSERT_EQ(group.get<char>(*(group.data() + 2u)), 'c');
+    ASSERT_EQ(group.get<char>(*(group.data() + 3u)), 'b');
+    ASSERT_EQ(group.get<char>(*(group.data() + 4u)), 'a');
 
     ASSERT_EQ((group.get<boxed_int, char>(entities[0])), (std::make_tuple(boxed_int{6}, 'c')));
     ASSERT_EQ((group.get<boxed_int, char>(entities[1])), (std::make_tuple(boxed_int{3}, 'b')));
@@ -993,15 +985,10 @@ TEST(OwningGroup, SortWithExclusionList) {
     ASSERT_EQ(*(group.data() + 2u), entities[1]);
     ASSERT_EQ(*(group.data() + 3u), entities[0]);
 
-    ASSERT_EQ((group.raw<boxed_int>() + 0u)->value, 4);
-    ASSERT_EQ((group.raw<boxed_int>() + 1u)->value, 3);
-    ASSERT_EQ((group.raw<boxed_int>() + 2u)->value, 1);
-    ASSERT_EQ((group.raw<boxed_int>() + 3u)->value, 0);
-
-    ASSERT_EQ(group.get<boxed_int>(entities[0]).value, 0);
-    ASSERT_EQ(group.get<boxed_int>(entities[1]).value, 1);
-    ASSERT_EQ(group.get<boxed_int>(entities[3]).value, 3);
-    ASSERT_EQ(group.get<boxed_int>(entities[4]).value, 4);
+    ASSERT_EQ(group.get<boxed_int>(*(group.data() + 0u)).value, 4);
+    ASSERT_EQ(group.get<boxed_int>(*(group.data() + 1u)).value, 3);
+    ASSERT_EQ(group.get<boxed_int>(*(group.data() + 2u)).value, 1);
+    ASSERT_EQ(group.get<boxed_int>(*(group.data() + 3u)).value, 0);
 
     ASSERT_FALSE(group.contains(entities[2]));
 }
@@ -1062,8 +1049,6 @@ TEST(OwningGroup, ConstNonConstAndAllInBetween) {
     static_assert(std::is_same_v<decltype(group.get<int, const char, double, const float>({})), std::tuple<int &, const char &, double &, const float &>>);
     static_assert(std::is_same_v<decltype(group.get({})), std::tuple<int &, const char &, double &, const float &>>);
     static_assert(std::is_same_v<decltype(group.data()), const entt::entity *>);
-    static_assert(std::is_same_v<decltype(group.raw<const char>()), const char *>);
-    static_assert(std::is_same_v<decltype(group.raw<int>()), int *>);
 
     group.each([](auto &&i, auto &&c, auto &&d, auto &&f) {
         static_assert(std::is_same_v<decltype(i), int &>);

+ 26 - 6
test/entt/entity/helper.cpp

@@ -41,19 +41,39 @@ TEST(Helper, Invoke) {
 
 TEST(Helper, ToEntity) {
     entt::registry registry;
+    const entt::entity null = entt::null;
+    const int value = 42;
+
+    ASSERT_EQ(entt::to_entity(registry, 42), null);
+    ASSERT_EQ(entt::to_entity(registry, value), null);
+
     const auto entity = registry.create();
+    registry.emplace<int>(entity);
+
+    while(registry.size<int>() < (ENTT_PAGE_SIZE - 1u)) {
+        registry.emplace<int>(registry.create(), value);
+    }
+
     const auto other = registry.create();
+    const auto next = registry.create();
 
-    registry.emplace<int>(entity);
     registry.emplace<int>(other);
-    registry.emplace<char>(other);
+    registry.emplace<int>(next);
 
     ASSERT_EQ(entt::to_entity(registry, registry.get<int>(entity)), entity);
     ASSERT_EQ(entt::to_entity(registry, registry.get<int>(other)), other);
-    ASSERT_EQ(entt::to_entity(registry, registry.get<char>(other)), other);
+    ASSERT_EQ(entt::to_entity(registry, registry.get<int>(next)), next);
 
-    registry.destroy(entity);
+    ASSERT_EQ(&registry.get<int>(entity) + ENTT_PAGE_SIZE - 1u, &registry.get<int>(other));
+    ASSERT_NE(&registry.get<int>(entity) + ENTT_PAGE_SIZE, &registry.get<int>(next));
 
-    ASSERT_EQ(entt::to_entity(registry, registry.get<int>(other)), other);
-    ASSERT_EQ(entt::to_entity(registry, registry.get<char>(other)), other);
+    registry.destroy(other);
+
+    ASSERT_EQ(entt::to_entity(registry, registry.get<int>(entity)), entity);
+    ASSERT_EQ(entt::to_entity(registry, registry.get<int>(next)), next);
+
+    ASSERT_EQ(&registry.get<int>(entity) + ENTT_PAGE_SIZE - 1u, &registry.get<int>(next));
+
+    ASSERT_EQ(entt::to_entity(registry, 42), null);
+    ASSERT_EQ(entt::to_entity(registry, value), null);
 }

+ 17 - 5
test/entt/entity/registry.cpp

@@ -154,8 +154,8 @@ TEST(Registry, Functionalities) {
     ASSERT_TRUE(registry.empty());
 
     ASSERT_EQ(registry.capacity(), 42u);
-    ASSERT_EQ(registry.capacity<int>(), 8u);
-    ASSERT_EQ(registry.capacity<char>(), 8u);
+    ASSERT_EQ(registry.capacity<int>(), ENTT_PAGE_SIZE);
+    ASSERT_EQ(registry.capacity<char>(), ENTT_PAGE_SIZE);
     ASSERT_EQ(registry.size<int>(), 0u);
     ASSERT_EQ(registry.size<char>(), 0u);
     ASSERT_TRUE((registry.empty<int, char>()));
@@ -295,8 +295,8 @@ TEST(Registry, Functionalities) {
     ASSERT_EQ(registry.size<char>(), 0u);
     ASSERT_TRUE(registry.empty<int>());
 
-    ASSERT_EQ(registry.capacity<int>(), 8u);
-    ASSERT_EQ(registry.capacity<char>(), 8u);
+    ASSERT_EQ(registry.capacity<int>(), ENTT_PAGE_SIZE);
+    ASSERT_EQ(registry.capacity<char>(), ENTT_PAGE_SIZE);
 
     registry.shrink_to_fit<int, char>();
 
@@ -530,13 +530,25 @@ TEST(Registry, VersionOverflow) {
     entt::registry registry;
     const auto entity = registry.create();
 
-    registry.destroy(entity, typename traits_type::version_type(traits_type::version_mask));
+    registry.destroy(entity);
+
+    ASSERT_NE(registry.current(entity), registry.version(entity));
+    ASSERT_NE(registry.current(entity), typename traits_type::version_type{});
+
+    registry.destroy(registry.create(), typename traits_type::version_type{traits_type::version_mask});
     registry.destroy(registry.create());
 
     ASSERT_EQ(registry.current(entity), registry.version(entity));
     ASSERT_EQ(registry.current(entity), typename traits_type::version_type{});
 }
 
+TEST(Registry, NullEntity) {
+    entt::registry registry;
+
+    ASSERT_FALSE(registry.valid(entt::null));
+    ASSERT_DEATH(static_cast<void>(registry.create(entt::null)), "");
+}
+
 TEST(Registry, Each) {
     entt::registry registry;
     entt::registry::size_type tot;

+ 0 - 1
test/entt/entity/registry_no_eto.cpp

@@ -14,7 +14,6 @@ TEST(Registry, NoEto) {
     registry.emplace<empty_type>(entity);
     registry.emplace<int>(entity, 42);
 
-    ASSERT_NE(registry.view<empty_type>().raw(), nullptr);
     ASSERT_NE(registry.try_get<empty_type>(entity), nullptr);
     ASSERT_EQ(registry.view<empty_type>().get(entity), std::as_const(registry).view<const empty_type>().get(entity));
 

+ 38 - 25
test/entt/entity/storage.cpp

@@ -40,7 +40,7 @@ TEST(Storage, Functionalities) {
 
     pool.reserve(42);
 
-    ASSERT_EQ(pool.capacity(), 42u);
+    ASSERT_EQ(pool.capacity(), ENTT_PAGE_SIZE);
     ASSERT_TRUE(pool.empty());
     ASSERT_EQ(pool.size(), 0u);
     ASSERT_EQ(std::as_const(pool).begin(), std::as_const(pool).end());
@@ -80,7 +80,7 @@ TEST(Storage, Functionalities) {
     ASSERT_FALSE(pool.contains(entt::entity{0}));
     ASSERT_FALSE(pool.contains(entt::entity{41}));
 
-    ASSERT_EQ(pool.capacity(), 42u);
+    ASSERT_EQ(pool.capacity(), ENTT_PAGE_SIZE);
 
     pool.shrink_to_fit();
 
@@ -208,6 +208,35 @@ TEST(Storage, Remove) {
     ASSERT_EQ(*pool.begin(), 1);
 }
 
+TEST(Storage, ShrinkToFit) {
+    entt::storage<int> pool;
+
+    for(std::size_t next{}; next < ENTT_PAGE_SIZE; ++next) {
+        pool.emplace(entt::entity(next));
+    }
+
+    pool.emplace(entt::entity{ENTT_PAGE_SIZE});
+    pool.erase(entt::entity{ENTT_PAGE_SIZE});
+
+    ASSERT_EQ(pool.capacity(), 2 * ENTT_PAGE_SIZE);
+    ASSERT_EQ(pool.size(), ENTT_PAGE_SIZE);
+
+    pool.shrink_to_fit();
+
+    ASSERT_EQ(pool.capacity(), ENTT_PAGE_SIZE);
+    ASSERT_EQ(pool.size(), ENTT_PAGE_SIZE);
+
+    pool.clear();
+
+    ASSERT_EQ(pool.capacity(), ENTT_PAGE_SIZE);
+    ASSERT_EQ(pool.size(), 0u);
+
+    pool.shrink_to_fit();
+
+    ASSERT_EQ(pool.capacity(), 0u);
+    ASSERT_EQ(pool.size(), 0u);
+}
+
 TEST(Storage, AggregatesMustWork) {
     struct aggregate_type { int value; };
     // the goal of this test is to enforce the requirements for aggregate types
@@ -389,22 +418,6 @@ TEST(Storage, ConstReverseIterator) {
     ASSERT_GE(cend, pool.crend());
 }
 
-TEST(Storage, Raw) {
-    entt::storage<int> pool;
-
-    pool.emplace(entt::entity{3}, 3);
-    pool.emplace(entt::entity{12}, 6);
-    pool.emplace(entt::entity{42}, 9);
-
-    ASSERT_EQ(pool.get(entt::entity{3}), 3);
-    ASSERT_EQ(std::as_const(pool).get(entt::entity{12}), 6);
-    ASSERT_EQ(pool.get(entt::entity{42}), 9);
-
-    ASSERT_EQ(pool.raw()[0u], 3);
-    ASSERT_EQ(std::as_const(pool).raw()[1u], 6);
-    ASSERT_EQ(pool.raw()[2u], 9);
-}
-
 TEST(Storage, SortOrdered) {
     entt::storage<boxed_int> pool;
     entt::entity entities[5u]{entt::entity{12}, entt::entity{42}, entt::entity{7}, entt::entity{3}, entt::entity{9}};
@@ -467,9 +480,9 @@ TEST(Storage, SortRange) {
 
     pool.sort_n(2u, [](auto lhs, auto rhs) { return lhs.value < rhs.value; });
 
-    ASSERT_EQ(pool.raw()[0u], boxed_int{6});
-    ASSERT_EQ(pool.raw()[1u], boxed_int{3});
-    ASSERT_EQ(pool.raw()[2u], boxed_int{1});
+    ASSERT_EQ(pool.rbegin()[0u], boxed_int{6});
+    ASSERT_EQ(pool.rbegin()[1u], boxed_int{3});
+    ASSERT_EQ(pool.rbegin()[2u], boxed_int{1});
 
     ASSERT_EQ(pool.data()[0u], entt::entity{42});
     ASSERT_EQ(pool.data()[1u], entt::entity{12});
@@ -650,12 +663,12 @@ TEST(Storage, CanModifyDuringIteration) {
     entt::storage<int> pool;
     pool.emplace(entt::entity{0}, 42);
 
-    ASSERT_EQ(pool.capacity(), 1u);
+    ASSERT_EQ(pool.capacity(), ENTT_PAGE_SIZE);
 
     const auto it = pool.cbegin();
-    pool.reserve(2u);
+    pool.reserve(ENTT_PAGE_SIZE + 1u);
 
-    ASSERT_EQ(pool.capacity(), 2u);
+    ASSERT_EQ(pool.capacity(), 2 * ENTT_PAGE_SIZE);
 
     // this should crash with asan enabled if we break the constraint
     const auto entity = *it;
@@ -696,7 +709,7 @@ TEST(Storage, MoveOnlyComponent) {
     (void)pool;
 }
 
-TEST(Storage, ConstructorExceptionDoesNotAddToStorage) {
+TEST(Storage, EmplaceStrongExceptionGuarantee) {
     entt::storage<throwing_component> pool;
 
     try {

+ 1 - 14
test/entt/entity/view.cpp

@@ -47,9 +47,6 @@ TEST(SingleComponentView, Functionalities) {
     ASSERT_EQ(*(view.data() + 0), e1);
     ASSERT_EQ(*(view.data() + 1), e0);
 
-    ASSERT_EQ(*(view.raw() + 0), '2');
-    ASSERT_EQ(*(cview.raw() + 1), '1');
-
     registry.erase<char>(e0);
     registry.erase<char>(e1);
 
@@ -64,7 +61,7 @@ TEST(SingleComponentView, Functionalities) {
     ASSERT_FALSE(invalid);
 }
 
-TEST(SingleComponentView, RawData) {
+TEST(SingleComponentView, Data) {
     entt::registry registry;
     auto view = registry.view<int>();
     auto cview = std::as_const(registry).view<const int>();
@@ -73,8 +70,6 @@ TEST(SingleComponentView, RawData) {
 
     ASSERT_EQ(view.size(), 0u);
     ASSERT_EQ(cview.size(), 0u);
-    ASSERT_EQ(view.raw(), nullptr);
-    ASSERT_EQ(cview.raw(), nullptr);
     ASSERT_EQ(view.data(), nullptr);
     ASSERT_EQ(cview.data(), nullptr);
 
@@ -82,8 +77,6 @@ TEST(SingleComponentView, RawData) {
 
     ASSERT_NE(view.size(), 0u);
     ASSERT_NE(cview.size(), 0u);
-    ASSERT_EQ(*view.raw(), 42);
-    ASSERT_EQ(*cview.raw(), 42);
     ASSERT_EQ(*view.data(), entity);
     ASSERT_EQ(*cview.data(), entity);
 
@@ -105,7 +98,6 @@ TEST(SingleComponentView, LazyTypeFromConstRegistry) {
     ASSERT_TRUE(cview);
     ASSERT_TRUE(eview);
 
-    ASSERT_NE(cview.raw(), nullptr);
     ASSERT_NE(eview.data(), nullptr);
 
     ASSERT_FALSE(cview.empty());
@@ -226,15 +218,10 @@ TEST(SingleComponentView, ConstNonConstAndAllInBetween) {
     ASSERT_EQ(view.size(), 1u);
     ASSERT_EQ(cview.size(), 1u);
 
-    static_assert(std::is_same_v<decltype(view.raw()), int *>);
-    static_assert(std::is_same_v<decltype(cview.raw()), const int *>);
-
     static_assert(std::is_same_v<decltype(view.get<int>({})), int &>);
     static_assert(std::is_same_v<decltype(view.get({})), std::tuple<int &>>);
-    static_assert(std::is_same_v<decltype(view.raw()), int *>);
     static_assert(std::is_same_v<decltype(cview.get<const int>({})), const int &>);
     static_assert(std::is_same_v<decltype(cview.get({})), std::tuple<const int &>>);
-    static_assert(std::is_same_v<decltype(cview.raw()), const int *>);
 
     view.each([](auto &&i) {
         static_assert(std::is_same_v<decltype(i), int &>);