Quellcode durchsuchen

empty type optimization

Michele Caini vor 7 Jahren
Ursprung
Commit
d32c8e9457

+ 3 - 1
TODO

@@ -17,7 +17,9 @@
 * allow for built-in parallel each if possible
 * add on-the-fly sort functionality (is it possible?)
 * write/show how to create an archetype based model on top of EnTT
+* write/show how to create prefabs on top of EnTT (that's easy!!)
 * mention hunter in the readme file, section packaging tools
 * travis + windows is now available, try it
 * events on replace, so that one can track updated components? indagate impact
-* add sparse_set::swap and registry::swap
+* optimize for empty component - maybe it's possible with a raw-less view that uses fake iterators (with correct size (correct size, same component)
+* tags revenge: if it's possible, reintroduce them but without a link to entities (see #169 for more details)

+ 28 - 0
docs/entity.md

@@ -35,6 +35,7 @@
   * [Types: const, non-const and all in between](#types-const-non-const-and-all-in-between)
   * [Give me everything](#give-me-everything)
 * [Iterations: what is allowed and what is not](#iterations-what-is-allowed-and-what-is-not)
+* [Empty type optimization](#empty-type-optimization)
 * [Multithreading](#multithreading)
   * [Views and Iterators](#views-and-iterators)
 <!--
@@ -1314,6 +1315,33 @@ To work around it, possible approaches are:
 A notable side effect of this feature is that the number of required allocations
 is further reduced in most of the cases.
 
+# Empty type optimization
+
+An empty type `T` is such that `std::is_empty_v<T>` returns true. They are also
+the same types for which _empty base optimization_ (EBO) is possibile.<br/>
+`EnTT` handles these types in a special way, optimizing both in terms of
+performance and memory usage. However, this also has drawbacks that are worth
+mentioning.
+
+When an empty type is detected, a pool is created in such a way that one and
+only one instance of the given type is created. All the entities will refer to
+it. Since the type is, in fact, empty, this is safe and there is no risk that a
+modification of its data members will affect other instances.<br/>
+Iterations are faster because only the entities to which the type is assigned
+are considered. Similarly, less memory is used, since there exists always only
+one instance of the component itself, no matter how many entities it is
+assigned.
+
+The drawback is that the `raw` member function will no longer be able to return
+a valid pointer to the list of components in the pool. This is because there is
+no list of components at all. Therefore, `raw` will always return `nullptr` for
+this type of components.<br/>
+Nonetheless, the iterators returned by the `begin` and `end` member functions
+are still valid and can be used safely. Similarly, raw views can still be built
+for this type of components if required.<br/>
+More in general, all the features offered by the library aren't affected, but
+for the raw member function that is no longer available instead.
+
 # Multithreading
 
 In general, the entire registry isn't thread safe as it is. Thread safety isn't

+ 4 - 0
src/entt/entity/registry.hpp

@@ -327,6 +327,10 @@ 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
+     * Empty components aren't explicitly instantiated. Therefore, this function
+     * always returns `nullptr` for them.
+     *
      * @tparam Component Type of component in which one is interested.
      * @return A pointer to the array of components of the given type.
      */

+ 185 - 39
src/entt/entity/sparse_set.hpp

@@ -518,7 +518,7 @@ class sparse_set<Entity, Type>: public sparse_set<Entity> {
     using underlying_type = sparse_set<Entity>;
     using traits_type = entt_traits<Entity>;
 
-    template<bool Const>
+    template<bool Const, bool = std::is_empty_v<Type>>
     class iterator final {
         friend class sparse_set<Entity, Type>;
 
@@ -626,6 +626,112 @@ class sparse_set<Entity, Type>: public sparse_set<Entity> {
         index_type index;
     };
 
+    template<bool Const>
+    class iterator<Const, true> final {
+        friend class sparse_set<Entity, Type>;
+
+        using instance_type = std::conditional_t<Const, const Type, Type>;
+        using index_type = typename traits_type::difference_type;
+
+        iterator(instance_type *instance, index_type index) ENTT_NOEXCEPT
+            : instance{instance}, index{index}
+        {}
+
+    public:
+        using difference_type = index_type;
+        using value_type = std::conditional_t<Const, const Type, Type>;
+        using pointer = value_type *;
+        using reference = value_type &;
+        using iterator_category = std::random_access_iterator_tag;
+
+        iterator() ENTT_NOEXCEPT = default;
+
+        iterator(const iterator &) ENTT_NOEXCEPT = default;
+        iterator(iterator &&) ENTT_NOEXCEPT = default;
+
+        iterator & operator=(const iterator &) ENTT_NOEXCEPT = default;
+        iterator & operator=(iterator &&) ENTT_NOEXCEPT = default;
+
+        iterator & operator++() ENTT_NOEXCEPT {
+            return --index, *this;
+        }
+
+        iterator operator++(int) ENTT_NOEXCEPT {
+            iterator orig = *this;
+            return ++(*this), orig;
+        }
+
+        iterator & operator--() ENTT_NOEXCEPT {
+            return ++index, *this;
+        }
+
+        iterator operator--(int) ENTT_NOEXCEPT {
+            iterator orig = *this;
+            return --(*this), orig;
+        }
+
+        iterator & operator+=(const difference_type value) ENTT_NOEXCEPT {
+            index -= value;
+            return *this;
+        }
+
+        iterator operator+(const difference_type value) const ENTT_NOEXCEPT {
+            return iterator{instance, index-value};
+        }
+
+        inline iterator & operator-=(const difference_type value) ENTT_NOEXCEPT {
+            return (*this += -value);
+        }
+
+        inline iterator operator-(const difference_type value) const ENTT_NOEXCEPT {
+            return (*this + -value);
+        }
+
+        difference_type operator-(const iterator &other) const ENTT_NOEXCEPT {
+            return other.index - index;
+        }
+
+        reference operator[](const difference_type) const ENTT_NOEXCEPT {
+            return *instance;
+        }
+
+        bool operator==(const iterator &other) const ENTT_NOEXCEPT {
+            return other.index == index;
+        }
+
+        inline bool operator!=(const iterator &other) const ENTT_NOEXCEPT {
+            return !(*this == other);
+        }
+
+        bool operator<(const iterator &other) const ENTT_NOEXCEPT {
+            return index > other.index;
+        }
+
+        bool operator>(const iterator &other) const ENTT_NOEXCEPT {
+            return index < other.index;
+        }
+
+        inline bool operator<=(const iterator &other) const ENTT_NOEXCEPT {
+            return !(*this > other);
+        }
+
+        inline bool operator>=(const iterator &other) const ENTT_NOEXCEPT {
+            return !(*this < other);
+        }
+
+        pointer operator->() const ENTT_NOEXCEPT {
+            return instance;
+        }
+
+        inline reference operator*() const ENTT_NOEXCEPT {
+            return *operator->();
+        }
+
+    private:
+        instance_type *instance;
+        index_type index;
+    };
+
 public:
     /*! @brief Type of the objects associated with the entities. */
     using object_type = Type;
@@ -646,9 +752,12 @@ public:
      *
      * @param cap Desired capacity.
      */
-    void reserve(const size_type cap) {
+    void reserve([[maybe_unused]] const size_type cap) {
         underlying_type::reserve(cap);
-        instances.reserve(cap);
+
+        if constexpr(!std::is_empty_v<object_type>) {
+            instances.reserve(cap);
+        }
     }
 
     /**
@@ -664,15 +773,23 @@ public:
      * performance boost but less guarantees. Use `begin` and `end` if you want
      * to iterate the sparse set in the expected order.
      *
+     * @warning
+     * Empty components aren't explicitly instantiated. Therefore, this function
+     * always returns `nullptr` for them.
+     *
      * @return A pointer to the array of objects.
      */
     const object_type * raw() const ENTT_NOEXCEPT {
-        return instances.data();
+        if constexpr(std::is_empty_v<object_type>) {
+            return nullptr;
+        } else {
+            return instances.data();
+        }
     }
 
     /*! @copydoc raw */
     object_type * raw() ENTT_NOEXCEPT {
-        return instances.data();
+        return const_cast<object_type *>(std::as_const(*this).raw());
     }
 
     /**
@@ -688,7 +805,7 @@ public:
      * @return An iterator to the first instance of the given type.
      */
     const_iterator_type cbegin() const ENTT_NOEXCEPT {
-        const typename traits_type::difference_type pos = instances.size();
+        const typename traits_type::difference_type pos = underlying_type::size();
         return const_iterator_type{&instances, pos};
     }
 
@@ -699,7 +816,7 @@ public:
 
     /*! @copydoc begin */
     iterator_type begin() ENTT_NOEXCEPT {
-        const typename traits_type::difference_type pos = instances.size();
+        const typename traits_type::difference_type pos = underlying_type::size();
         return iterator_type{&instances, pos};
     }
 
@@ -743,8 +860,13 @@ public:
      * @param entity A valid entity identifier.
      * @return The object associated with the entity.
      */
-    const object_type & get(const entity_type entity) const ENTT_NOEXCEPT {
-        return instances[underlying_type::get(entity)];
+    const object_type & get([[maybe_unused]] const entity_type entity) const ENTT_NOEXCEPT {
+        if constexpr(std::is_empty_v<object_type>) {
+            assert(underlying_type::has(entity));
+            return instances;
+        } else {
+            return instances[underlying_type::get(entity)];
+        }
     }
 
     /*! @copydoc get */
@@ -758,7 +880,11 @@ public:
      * @return The object associated with the entity, if any.
      */
     const object_type * try_get(const entity_type entity) const ENTT_NOEXCEPT {
-        return underlying_type::has(entity) ? (instances.data() + underlying_type::get(entity)) : nullptr;
+        if constexpr(std::is_empty_v<object_type>) {
+            return underlying_type::has(entity) ? &instances : nullptr;
+        } else {
+            return underlying_type::has(entity) ? (instances.data() + underlying_type::get(entity)) : nullptr;
+        }
     }
 
     /*! @copydoc try_get */
@@ -786,16 +912,20 @@ public:
      * @return The object associated with the entity.
      */
     template<typename... Args>
-    object_type & construct(const entity_type entity, Args &&... args) {
+    object_type & construct([[maybe_unused]] const entity_type entity, [[maybe_unused]] Args &&... args) {
         underlying_type::construct(entity);
 
-        if constexpr(std::is_aggregate_v<Type>) {
-            instances.emplace_back(Type{std::forward<Args>(args)...});
+        if constexpr(std::is_empty_v<object_type>) {
+            return instances;
         } else {
-            instances.emplace_back(std::forward<Args>(args)...);
-        }
+            if constexpr(std::is_aggregate_v<object_type>) {
+                instances.emplace_back(Type{std::forward<Args>(args)...});
+            } else {
+                instances.emplace_back(std::forward<Args>(args)...);
+            }
 
-        return instances.back();
+            return instances.back();
+        }
     }
 
     /**
@@ -810,11 +940,14 @@ public:
      * @param entity A valid entity identifier.
      */
     void destroy(const entity_type entity) override {
-        // swapping isn't required here, we are getting rid of the last element
-        // however, we must protect ourselves from self assignments (see #37)
-        auto tmp = std::move(instances.back());
-        instances[underlying_type::get(entity)] = std::move(tmp);
-        instances.pop_back();
+        if constexpr(!std::is_empty_v<object_type>) {
+            // swapping isn't required here, we are getting rid of the last element
+            // however, we must protect ourselves from self assignments (see #37)
+            auto tmp = std::move(instances.back());
+            instances[underlying_type::get(entity)] = std::move(tmp);
+            instances.pop_back();
+        }
+
         underlying_type::destroy(entity);
     }
 
@@ -852,6 +985,10 @@ public:
      * 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.
@@ -860,7 +997,9 @@ public:
      * @param args Arguments to forward to the sort function object, if any.
      */
     template<typename Compare, typename Sort = std_sort, typename... Args>
-    void sort(Compare compare, Sort sort = Sort{}, Args &&... args) {
+    void sort([[maybe_unused]] Compare compare, [[maybe_unused]] Sort sort = Sort{}, [[maybe_unused]] Args &&... args) {
+        static_assert(!std::is_empty_v<object_type>);
+
         std::vector<size_type> copy(instances.size());
         std::iota(copy.begin(), copy.end(), 0);
 
@@ -908,33 +1047,40 @@ public:
      * @param other The sparse sets that imposes the order of the entities.
      */
     void respect(const sparse_set<Entity> &other) ENTT_NOEXCEPT {
-        const auto to = other.end();
-        auto from = other.begin();
+        if constexpr(std::is_empty_v<object_type>) {
+            underlying_type::respect(other);
+        } else {
+            const auto to = other.end();
+            auto from = other.begin();
 
-        size_type pos = underlying_type::size() - 1;
-        const auto *local = underlying_type::data();
+            size_type pos = underlying_type::size() - 1;
+            const auto *local = underlying_type::data();
 
-        while(pos && from != to) {
-            const auto curr = *from;
+            while(pos && from != to) {
+                const auto curr = *from;
 
-            if(underlying_type::has(curr)) {
-                if(curr != *(local + pos)) {
-                    auto candidate = underlying_type::get(curr);
-                    std::swap(instances[pos], instances[candidate]);
-                    underlying_type::swap(pos, candidate);
+                if(underlying_type::has(curr)) {
+                    if(curr != *(local + pos)) {
+                        auto candidate = underlying_type::get(curr);
+                        std::swap(instances[pos], instances[candidate]);
+                        underlying_type::swap(pos, candidate);
+                    }
+
+                    --pos;
                 }
 
-                --pos;
+                ++from;
             }
-
-            ++from;
         }
     }
 
     /*! @brief Resets a sparse set. */
     void reset() override {
         underlying_type::reset();
-        instances.clear();
+
+        if constexpr(!std::is_empty_v<object_type>) {
+            instances.clear();
+        }
     }
 
     /**
@@ -948,7 +1094,7 @@ public:
      * copyable, an empty unique pointer otherwise.
      */
     std::unique_ptr<sparse_set<Entity>> clone() const override {
-        if constexpr(std::is_copy_constructible_v<Type>) {
+        if constexpr(std::is_copy_constructible_v<object_type>) {
             return std::make_unique<sparse_set>(*this);
         } else {
             return nullptr;
@@ -956,7 +1102,7 @@ public:
     }
 
 private:
-    std::vector<object_type> instances;
+    std::conditional_t<std::is_empty_v<object_type>, object_type, std::vector<object_type>> instances;
 };
 
 

+ 19 - 1
src/entt/entity/view.hpp

@@ -100,10 +100,20 @@ class persistent_view final {
         });
     }
 
+    template<typename Comp>
+    inline decltype(auto) get([[maybe_unused]] typename persistent_type::object_type::value_type index, [[maybe_unused]] const Entity entity) const {
+        if constexpr(std::is_empty_v<Comp>) {
+            // raw returns nullptr for empty components
+            return pool<Comp>()->get(entity);
+        } else {
+            return pool<Comp>()->raw()[index];
+        }
+    }
+
     template<typename Func, std::size_t... Indexes>
     void each(Func func, std::index_sequence<Indexes...>) const {
         std::for_each(handler->view_type::begin(), handler->view_type::end(), [func = std::move(func), raw = handler->cbegin(), this](const auto entity) mutable {
-            func(entity, pool<Component>()->raw()[(*raw)[Indexes]]...);
+            func(entity, get<Component>((*raw)[Indexes], entity)...);
             ++raw;
         });
     }
@@ -744,6 +754,10 @@ public:
      * There are no guarantees on the order of the components. Use `begin` and
      * `end` if you want to iterate the view in the expected order.
      *
+     * @warning
+     * Empty components aren't explicitly instantiated. Therefore, this function
+     * always returns `nullptr` for them.
+     *
      * @return A pointer to the array of components.
      */
     raw_type * raw() const ENTT_NOEXCEPT {
@@ -974,6 +988,10 @@ public:
      * There are no guarantees on the order of the components. Use `begin` and
      * `end` if you want to iterate the view in the expected order.
      *
+     * @warning
+     * Empty components aren't explicitly instantiated. Therefore, this function
+     * always returns `nullptr` for them.
+     *
      * @return A pointer to the array of components.
      */
     raw_type * raw() const ENTT_NOEXCEPT {

+ 144 - 2
test/entt/entity/sparse_set.cpp

@@ -400,6 +400,17 @@ TEST(SparseSetWithType, Functionalities) {
     other = std::move(set);
 }
 
+TEST(SparseSetWithType, FunctionalitiesEmptyType) {
+    struct empty_type {};
+    entt::sparse_set<std::uint64_t, empty_type> set;
+    const auto &cset = set;
+
+    ASSERT_EQ(&set.construct(42), &set.construct(99));
+    ASSERT_EQ(&set.get(42), set.try_get(42));
+    ASSERT_EQ(&set.get(42), &set.get(99));
+    ASSERT_EQ(cset.try_get(42), cset.try_get(99));
+}
+
 TEST(SparseSetWithType, AggregatesMustWork) {
     struct aggregate_type { int value; };
     // the goal of this test is to enforce the requirements for aggregate types
@@ -501,22 +512,130 @@ TEST(SparseSetWithType, ConstIterator) {
     ASSERT_GE(cend, set.cend());
 }
 
+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);
+
+    iterator_type end{set.begin()};
+    iterator_type begin{};
+    begin = set.end();
+    std::swap(begin, end);
+
+    ASSERT_EQ(begin, set.begin());
+    ASSERT_EQ(end, set.end());
+    ASSERT_NE(begin, end);
+
+    ASSERT_EQ(begin++, set.begin());
+    ASSERT_EQ(begin--, set.end());
+
+    ASSERT_EQ(begin+1, set.end());
+    ASSERT_EQ(end-1, set.begin());
+
+    ASSERT_EQ(++begin, set.end());
+    ASSERT_EQ(--begin, set.begin());
+
+    ASSERT_EQ(begin += 1, set.end());
+    ASSERT_EQ(begin -= 1, set.begin());
+
+    ASSERT_EQ(begin + (end - begin), set.end());
+    ASSERT_EQ(begin - (begin - end), set.end());
+
+    ASSERT_EQ(end - (end - begin), set.begin());
+    ASSERT_EQ(end + (begin - end), set.begin());
+
+    ASSERT_EQ(&begin[0], set.begin().operator->());
+
+    ASSERT_LT(begin, end);
+    ASSERT_LE(begin, set.begin());
+
+    ASSERT_GT(end, begin);
+    ASSERT_GE(end, set.end());
+
+    set.construct(33);
+
+    ASSERT_EQ(&*begin, &*(begin+1));
+    ASSERT_EQ(&begin[0], &begin[1]);
+    ASSERT_EQ(begin.operator->(), (end-1).operator->());
+}
+
+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);
+
+    iterator_type cend{set.cbegin()};
+    iterator_type cbegin{};
+    cbegin = set.cend();
+    std::swap(cbegin, cend);
+
+    ASSERT_EQ(cbegin, set.cbegin());
+    ASSERT_EQ(cend, set.cend());
+    ASSERT_NE(cbegin, cend);
+
+    ASSERT_EQ(cbegin++, set.cbegin());
+    ASSERT_EQ(cbegin--, set.cend());
+
+    ASSERT_EQ(cbegin+1, set.cend());
+    ASSERT_EQ(cend-1, set.cbegin());
+
+    ASSERT_EQ(++cbegin, set.cend());
+    ASSERT_EQ(--cbegin, set.cbegin());
+
+    ASSERT_EQ(cbegin += 1, set.cend());
+    ASSERT_EQ(cbegin -= 1, set.cbegin());
+
+    ASSERT_EQ(cbegin + (cend - cbegin), set.cend());
+    ASSERT_EQ(cbegin - (cbegin - cend), set.cend());
+
+    ASSERT_EQ(cend - (cend - cbegin), set.cbegin());
+    ASSERT_EQ(cend + (cbegin - cend), set.cbegin());
+
+    ASSERT_EQ(&cbegin[0], set.cbegin().operator->());
+
+    ASSERT_LT(cbegin, cend);
+    ASSERT_LE(cbegin, set.cbegin());
+
+    ASSERT_GT(cend, cbegin);
+    ASSERT_GE(cend, set.cend());
+
+    set.construct(33);
+
+    ASSERT_EQ(&*cbegin, &*(cbegin+1));
+    ASSERT_EQ(&cbegin[0], &cbegin[1]);
+    ASSERT_EQ(cbegin.operator->(), (cend-1).operator->());
+}
+
 TEST(SparseSetWithType, Raw) {
     entt::sparse_set<std::uint64_t, int> set;
+    const auto &cset = set;
 
     set.construct(3, 3);
     set.construct(12, 6);
     set.construct(42, 9);
 
     ASSERT_EQ(set.get(3), 3);
-    ASSERT_EQ(set.get(12), 6);
+    ASSERT_EQ(cset.get(12), 6);
     ASSERT_EQ(set.get(42), 9);
 
     ASSERT_EQ(*(set.raw() + 0u), 3);
-    ASSERT_EQ(*(set.raw() + 1u), 6);
+    ASSERT_EQ(*(cset.raw() + 1u), 6);
     ASSERT_EQ(*(set.raw() + 2u), 9);
 }
 
+TEST(SparseSetWithType, RawEmptyType) {
+    struct empty_type {};
+    entt::sparse_set<std::uint64_t, empty_type> set;
+    const auto &cset = set;
+
+    set.construct(3);
+
+    ASSERT_EQ(set.raw(), nullptr);
+    ASSERT_EQ(cset.raw(), nullptr);
+}
+
 TEST(SparseSetWithType, SortOrdered) {
     entt::sparse_set<std::uint64_t, int> set;
 
@@ -821,6 +940,29 @@ TEST(SparseSetWithType, RespectUnordered) {
     ASSERT_EQ(*(rhs.data() + 5u), 5u);
 }
 
+TEST(SparseSetWithType, RespectOverlapEmptyType) {
+    struct empty_type {};
+    entt::sparse_set<std::uint64_t, empty_type> lhs;
+    entt::sparse_set<std::uint64_t, empty_type> rhs;
+    const auto &clhs = lhs;
+
+    lhs.construct(3);
+    lhs.construct(12);
+    lhs.construct(42);
+
+    rhs.construct(12);
+
+    ASSERT_EQ(lhs.sparse_set<std::uint64_t>::get(3), 0u);
+    ASSERT_EQ(lhs.sparse_set<std::uint64_t>::get(12), 1u);
+    ASSERT_EQ(lhs.sparse_set<std::uint64_t>::get(42), 2u);
+
+    lhs.respect(rhs);
+
+    ASSERT_EQ(clhs.sparse_set<std::uint64_t>::get(3), 0u);
+    ASSERT_EQ(clhs.sparse_set<std::uint64_t>::get(12), 2u);
+    ASSERT_EQ(clhs.sparse_set<std::uint64_t>::get(42), 1u);
+}
+
 TEST(SparseSetWithType, CanModifyDuringIteration) {
     entt::sparse_set<std::uint64_t, int> set;
     set.construct(0, 42);

+ 23 - 0
test/entt/entity/view.cpp

@@ -317,6 +317,29 @@ TEST(PersistentView, ExcludedComponents) {
     }
 }
 
+TEST(PersistentView, EmptyAndNonEmptyTypes) {
+    struct empty_type {};
+    entt::registry<> registry;
+    const auto view = registry.persistent_view<int, empty_type>();
+
+    const auto e0 = registry.create();
+    registry.assign<empty_type>(e0);
+    registry.assign<int>(e0);
+
+    const auto e1 = registry.create();
+    registry.assign<empty_type>(e1);
+    registry.assign<int>(e1);
+
+    registry.assign<int>(registry.create());
+
+    for(const auto entity: view) {
+        ASSERT_TRUE(entity == e0 || entity == e1);
+    }
+
+    ASSERT_EQ(view.size(), typename decltype(view)::size_type{2});
+    ASSERT_EQ(&view.get<empty_type>(e0), &view.get<empty_type>(e1));
+}
+
 TEST(SingleComponentView, Functionalities) {
     entt::registry<> registry;
     auto view = registry.view<char>();