Browse Source

view: added iterable object to visit entities and components at once

Michele Caini 5 years ago
parent
commit
4d1952a406
2 changed files with 477 additions and 12 deletions
  1. 340 6
      src/entt/entity/view.hpp
  2. 137 6
      test/entt/entity/view.cpp

+ 340 - 6
src/entt/entity/view.hpp

@@ -20,6 +20,287 @@
 namespace entt {
 
 
+/**
+ * @brief View range.
+ *
+ * Primary template isn't defined on purpose. All the specializations give a
+ * compile-time error, but for a few reasonable cases.
+ */
+template<typename...>
+class view_range;
+
+
+/**
+ * @brief Multi component view range.
+ *
+ * Iterable object to use to _visit_ a view.
+ *
+ * @sa view
+ *
+ * @tparam Entity A valid entity type (see entt_traits for more details).
+ * @tparam Exclude Types of components used to filter the view.
+ * @tparam Component Types of components iterated by the view.
+ */
+template<typename Entity, typename... Exclude, typename... Component>
+class view_range<Entity, exclude_t<Exclude...>, Component...> {
+    /*! @brief A view is allowed to create ranges. */
+    friend class basic_view<Entity, exclude_t<Exclude...>, Component...>;
+
+    // I could have used std::conditional_t ...
+    template<typename Comp>
+    struct pool { using type = storage<Entity, Comp>; };
+
+    // ... if only MSVC didn't have a bug ...
+    template<typename Comp>
+    struct pool<const Comp> { using type = const storage<Entity, std::remove_const_t<Comp>>; };
+
+    // ... that forces me to do the same in a worse way! :(
+    template<typename Comp>
+    using pool_type = typename pool<Comp>::type;
+
+    using entity_iterator = typename sparse_set<Entity>::iterator;
+    using filter_type = std::array<const sparse_set<Entity> *, sizeof...(Exclude)>;
+
+    class range_iterator {
+        friend class view_range<Entity, exclude_t<Exclude...>, Component...>;
+
+        range_iterator(entity_iterator from, entity_iterator to, std::tuple<pool_type<Component> *...> get, filter_type exclude) ENTT_NOEXCEPT
+            : first{from},
+              last{to},
+              pools{get},
+              filter{exclude}
+        {
+            if(first != last && !valid()) {
+                ++(*this);
+            }
+        }
+
+        [[nodiscard]] bool valid() const {
+            return (std::get<pool_type<Component> *>(pools)->contains(*first) && ...)
+                    && std::none_of(filter.cbegin(), filter.cend(), [entt = *first](const sparse_set<Entity> *curr) { return curr->contains(entt); });
+        }
+
+    public:
+        using difference_type = std::ptrdiff_t;
+        using value_type = decltype(std::tuple_cat(
+            std::declval<std::tuple<Entity>>(),
+            std::declval<std::conditional_t<ENTT_IS_EMPTY(Component), std::tuple<>, std::tuple<Component &>>>()...
+        ));
+        using pointer = void;
+        using reference = value_type;
+        using iterator_category = std::input_iterator_tag;
+
+        range_iterator() ENTT_NOEXCEPT = default;
+
+        range_iterator & operator++() ENTT_NOEXCEPT {
+            while(++first != last && !valid());
+            return *this;
+        }
+
+        range_iterator operator++(int) ENTT_NOEXCEPT {
+            range_iterator orig = *this;
+            return ++(*this), orig;
+        }
+
+        [[nodiscard]] reference operator*() const ENTT_NOEXCEPT {
+            return std::tuple_cat(std::make_tuple(*first), [this](auto *cpool) -> decltype(auto) {
+                if constexpr(ENTT_IS_EMPTY(typename std::decay_t<decltype(*cpool)>::object_type)) {
+                    return std::make_tuple();
+                } else {
+                    return std::forward_as_tuple(cpool->get(*first));
+                }
+            }(std::get<pool_type<Component> *>(pools))...);
+        }
+
+        [[nodiscard]] bool operator==(const range_iterator &other) const ENTT_NOEXCEPT {
+            return other.first == first;
+        }
+
+        [[nodiscard]] bool operator!=(const range_iterator &other) const ENTT_NOEXCEPT {
+            return !(*this == other);
+        }
+
+    private:
+        entity_iterator first;
+        const entity_iterator last;
+        std::tuple<pool_type<Component> *...> pools;
+        filter_type filter;
+    };
+
+    view_range(const sparse_set<Entity> &ref, std::tuple<pool_type<Component> *...> component, filter_type epool)
+        : candidate{&ref},
+          pools{component},
+          filter{epool}
+    {}
+
+public:
+    /*! @brief Underlying entity identifier. */
+    using entity_type = Entity;
+    /*! @brief Input iterator type. */
+    using iterator = range_iterator;
+
+    /**
+     * @brief Returns an iterator to the first element.
+     *
+     * The returned iterator points to the first element. If the range is empty,
+     * the returned iterator will be equal to `end()`.
+     *
+     * @note
+     * Iterators stay true to the order imposed to the underlying data
+     * structures.
+     *
+     * @return An iterator to the first element.
+     */
+    [[nodiscard]] iterator begin() const ENTT_NOEXCEPT {
+        return iterator{candidate->begin(), candidate->end(), pools, filter};
+    }
+
+    /**
+     * @brief Returns an iterator that is past the last element.
+     *
+     * The returned iterator points to the element following the last element.
+     * Attempting to dereference the returned iterator results in undefined
+     * behavior.
+     *
+     * @note
+     * Iterators stay true to the order imposed to the underlying data
+     * structures.
+     *
+     * @return An iterator to the element following the last element that has
+     * the given components.
+     */
+    [[nodiscard]] iterator end() const ENTT_NOEXCEPT {
+        return iterator{candidate->end(), candidate->end(), pools, filter};
+    }
+
+private:
+    const sparse_set<Entity> *candidate;
+    std::tuple<pool_type<Component> *...> pools;
+    filter_type filter;
+};
+
+
+/**
+ * @brief Single component view range specialization.
+ *
+ * Iterable object to use to _visit_ a view.
+ *
+ * @sa view
+ *
+ * @tparam Entity A valid entity type (see entt_traits for more details).
+ * @tparam Component Type of component iterated by the view.
+ */
+template<typename Entity, typename Component>
+class view_range<Entity, exclude_t<>, Component> {
+    /*! @brief A view is allowed to create ranges. */
+    friend class basic_view<Entity, exclude_t<>, Component>;
+
+    using pool_type = std::conditional_t<std::is_const_v<Component>, const storage<Entity, std::remove_const_t<Component>>, storage<Entity, Component>>;
+
+    class range_iterator {
+        friend class view_range<Entity, exclude_t<>, Component>;
+
+        using it_type = std::conditional_t<
+            ENTT_IS_EMPTY(Component),
+            std::tuple<typename sparse_set<Entity>::iterator>,
+            std::tuple<typename sparse_set<Entity>::iterator, decltype(std::declval<pool_type>().begin())>
+        >;
+
+        range_iterator(it_type from) ENTT_NOEXCEPT
+            : it{from}
+        {}
+
+    public:
+        using difference_type = std::ptrdiff_t;
+        using value_type = std::conditional_t<ENTT_IS_EMPTY(Component), std::tuple<Entity>, std::tuple<Entity, Component &>>;
+        using pointer = void;
+        using reference = value_type;
+        using iterator_category = std::input_iterator_tag;
+
+        range_iterator() ENTT_NOEXCEPT = default;
+
+        range_iterator & operator++() ENTT_NOEXCEPT {
+            return std::apply([](auto &&... curr) { (++curr, ...); }, it), *this;
+        }
+
+        range_iterator operator++(int) ENTT_NOEXCEPT {
+            range_iterator orig = *this;
+            return ++(*this), orig;
+        }
+
+        [[nodiscard]] reference operator*() const ENTT_NOEXCEPT {
+            return std::apply([](auto &&... curr) { return reference{*curr...}; }, it);
+        }
+
+        [[nodiscard]] bool operator==(const range_iterator &other) const ENTT_NOEXCEPT {
+            return std::get<0>(other.it) == std::get<0>(it);
+        }
+
+        [[nodiscard]] bool operator!=(const range_iterator &other) const ENTT_NOEXCEPT {
+            return !(*this == other);
+        }
+
+    private:
+        it_type it;
+    };
+
+    view_range(pool_type &ref)
+        : pool{&ref}
+    {}
+
+public:
+    /*! @brief Underlying entity identifier. */
+    using entity_type = Entity;
+    /*! @brief Input iterator type. */
+    using iterator = range_iterator;
+
+    /**
+     * @brief Returns an iterator to the first element.
+     *
+     * The returned iterator points to the first element. If the range is empty,
+     * the returned iterator will be equal to `end()`.
+     *
+     * @note
+     * Iterators stay true to the order imposed to the underlying data
+     * structures.
+     *
+     * @return An iterator to the first element.
+     */
+    [[nodiscard]] iterator begin() const ENTT_NOEXCEPT {
+        if constexpr(ENTT_IS_EMPTY(Component)) {
+            return iterator{std::make_tuple(pool->sparse_set<entity_type>::begin())};
+        } else {
+            return iterator{std::make_tuple(pool->sparse_set<entity_type>::begin(), pool->begin())};
+        }
+    }
+
+    /**
+     * @brief Returns an iterator that is past the last element.
+     *
+     * The returned iterator points to the element following the last element.
+     * Attempting to dereference the returned iterator results in undefined
+     * behavior.
+     *
+     * @note
+     * Iterators stay true to the order imposed to the underlying data
+     * structures.
+     *
+     * @return An iterator to the element following the last element that has
+     * the given components.
+     */
+    [[nodiscard]] iterator end() const ENTT_NOEXCEPT {
+        if constexpr(ENTT_IS_EMPTY(Component)) {
+            return iterator{std::make_tuple(pool->sparse_set<entity_type>::end())};
+        } else {
+            return iterator{std::make_tuple(pool->sparse_set<entity_type>::end(), pool->end())};
+        }
+    }
+
+private:
+    pool_type *pool;
+};
+
+
 /**
  * @brief View.
  *
@@ -219,7 +500,7 @@ public:
     using entity_type = Entity;
     /*! @brief Unsigned integer type. */
     using size_type = std::size_t;
-    /*! @brief Input iterator type. */
+    /*! @brief Bidirectional iterator type. */
     using iterator = view_iterator;
 
     /**
@@ -309,7 +590,7 @@ public:
      * `end()`.
      *
      * @note
-     * Input iterators stay true to the order imposed to the underlying data
+     * Iterators stay true to the order imposed to the underlying data
      * structures.
      *
      * @return An iterator to the first entity that has the given components.
@@ -328,7 +609,7 @@ public:
      * results in undefined behavior.
      *
      * @note
-     * Input iterators stay true to the order imposed to the underlying data
+     * Iterators stay true to the order imposed to the underlying data
      * structures.
      *
      * @return An iterator to the entity following the last entity that has the
@@ -462,6 +743,42 @@ public:
         traverse<Comp>(std::move(func), non_empty_type{});
     }
 
+    /**
+     * @brief Returns an iterable object to use to _visit_ the view.
+     *
+     * The iterable object returns tuples that contain the current entity and a
+     * set of references to its non-empty components. The _constness_ of the
+     * components is as requested.
+     *
+     * @note
+     * Empty types aren't explicitly instantiated and therefore they are never
+     * returned during iterations.
+     *
+     * @return An iterable object to use to _visit_ the view.
+     */
+    [[nodiscard]] view_range<entity_type, exclude_t<Exclude...>, Component...> each() const ENTT_NOEXCEPT {
+        return { candidate(), pools, filter };
+    }
+
+    /**
+     * @brief Returns an iterable object to use to _visit_ the view.
+     *
+     * The pool of the suggested component is used to lead the iterations. The
+     * returned elements will therefore respect the order of the pool associated
+     * with that type.<br/>
+     * It is no longer guaranteed that the performance is the best possible, but
+     * there will be greater control over the order of iteration.
+     *
+     * @sa each
+     *
+     * @tparam Comp Type of component to use to enforce the iteration order.
+     * @return An iterable object to use to _visit_ the view.
+     */
+    template<typename Comp>
+    [[nodiscard]] view_range<entity_type, exclude_t<Exclude...>, Component...> each() const ENTT_NOEXCEPT {
+        return { *std::get<pool_type<Comp> *>(pools), pools, filter };
+    }
+
 private:
     const std::tuple<pool_type<Component> *...> pools;
     filter_type filter;
@@ -517,7 +834,7 @@ public:
     using entity_type = Entity;
     /*! @brief Unsigned integer type. */
     using size_type = std::size_t;
-    /*! @brief Input iterator type. */
+    /*! @brief Random access iterator type. */
     using iterator = typename sparse_set<Entity>::iterator;
 
     /**
@@ -577,7 +894,7 @@ public:
      * `end()`.
      *
      * @note
-     * Input iterators stay true to the order imposed to the underlying data
+     * Iterators stay true to the order imposed to the underlying data
      * structures.
      *
      * @return An iterator to the first entity that has the given component.
@@ -595,7 +912,7 @@ public:
      * results in undefined behavior.
      *
      * @note
-     * Input iterators stay true to the order imposed to the underlying data
+     * Iterators stay true to the order imposed to the underlying data
      * structures.
      *
      * @return An iterator to the entity following the last entity that has the
@@ -725,6 +1042,23 @@ public:
         }
     }
 
+    /**
+     * @brief Returns an iterable object to use to _visit_ the view.
+     *
+     * The iterable object returns tuples that contain the current entity and a
+     * reference to its component if it's a non-empty one. The _constness_ of
+     * the component is as requested.
+     *
+     * @note
+     * Empty types aren't explicitly instantiated and therefore they are never
+     * returned during iterations.
+     *
+     * @return An iterable object to use to _visit_ the view.
+     */
+    [[nodiscard]] view_range<entity_type, exclude_t<>, Component> each() const ENTT_NOEXCEPT {
+        return { *pool };
+    }
+
 private:
     pool_type *pool;
 };

+ 137 - 6
test/entt/entity/view.cpp

@@ -108,15 +108,28 @@ TEST(SingleComponentView, Each) {
     registry.emplace<int>(registry.create());
 
     auto view = registry.view<int>();
+    auto cview = std::as_const(registry).view<const int>();
     std::size_t cnt = 0;
 
     view.each([&cnt](auto, int &) { ++cnt; });
     view.each([&cnt](int &) { ++cnt; });
 
-    ASSERT_EQ(cnt, std::size_t{4});
+    for([[maybe_unused]] auto curr: view.each()) {
+        ASSERT_TRUE((std::is_same_v<std::tuple_element_t<0, decltype(curr)>, entt::entity>));
+        ASSERT_TRUE((std::is_same_v<std::tuple_element_t<1, decltype(curr)>, int &>));
+        ++cnt;
+    }
+
+    ASSERT_EQ(cnt, std::size_t{6});
 
-    std::as_const(view).each([&cnt](auto, const int &) { --cnt; });
-    std::as_const(view).each([&cnt](const int &) { --cnt; });
+    cview.each([&cnt](auto, const int &) { --cnt; });
+    cview.each([&cnt](const int &) { --cnt; });
+
+    for([[maybe_unused]] auto curr: cview.each()) {
+        ASSERT_TRUE((std::is_same_v<std::tuple_element_t<0, decltype(curr)>, entt::entity>));
+        ASSERT_TRUE((std::is_same_v<std::tuple_element_t<1, decltype(curr)>, const int &>));
+        --cnt;
+    }
 
     ASSERT_EQ(cnt, std::size_t{0});
 }
@@ -149,6 +162,16 @@ TEST(SingleComponentView, ConstNonConstAndAllInBetween) {
     cview.each([](auto &&i) {
         ASSERT_TRUE((std::is_same_v<decltype(i), const int &>));
     });
+
+    for(auto curr: view.each()) {
+        ASSERT_TRUE((std::is_same_v<std::tuple_element_t<0, decltype(curr)>, entt::entity>));
+        ASSERT_TRUE((std::is_same_v<std::tuple_element_t<1, decltype(curr)>, int &>));
+    }
+
+    for(auto curr: cview.each()) {
+        ASSERT_TRUE((std::is_same_v<std::tuple_element_t<0, decltype(curr)>, entt::entity>));
+        ASSERT_TRUE((std::is_same_v<std::tuple_element_t<1, decltype(curr)>, const int &>));
+    }
 }
 
 TEST(SingleComponentView, Find) {
@@ -191,7 +214,7 @@ TEST(SingleComponentView, Find) {
     ASSERT_EQ(view.find(e4), view.end());
 }
 
-TEST(SingleComponentView, EachWithEmptyTypes) {
+TEST(SingleComponentView, EmptyTypes) {
     entt::registry registry;
     auto create = [&](auto... component) {
         const auto entt = registry.create();
@@ -211,6 +234,12 @@ TEST(SingleComponentView, EachWithEmptyTypes) {
         check = false;
     });
 
+    for(auto curr: registry.view<empty_type>().each()) {
+        ASSERT_EQ(std::tuple_size_v<decltype(curr)>, 1);
+        ASSERT_TRUE((std::is_same_v<std::tuple_element_t<0, decltype(curr)>, entt::entity>));
+        ASSERT_EQ(entity, std::get<0>(curr));
+    }
+
     registry.view<int>().each([entity](const auto entt, int) {
         ASSERT_EQ(entity, entt);
     });
@@ -219,6 +248,13 @@ TEST(SingleComponentView, EachWithEmptyTypes) {
         ASSERT_TRUE(check);
         check = false;
     });
+
+    for(auto curr: registry.view<int>().each()) {
+        ASSERT_EQ(std::tuple_size_v<decltype(curr)>, 2);
+        ASSERT_TRUE((std::is_same_v<std::tuple_element_t<0, decltype(curr)>, entt::entity>));
+        ASSERT_TRUE((std::is_same_v<std::tuple_element_t<1, decltype(curr)>, int &>));
+        ASSERT_EQ(entity, std::get<0>(curr));
+    }
 }
 
 TEST(SingleComponentView, FrontBack) {
@@ -375,11 +411,25 @@ TEST(MultiComponentView, Each) {
     view.each([&cnt](auto, int &, char &) { ++cnt; });
     view.each([&cnt](int &, char &) { ++cnt; });
 
-    ASSERT_EQ(cnt, std::size_t{4});
+    for(auto curr: view.each()) {
+        ASSERT_TRUE((std::is_same_v<std::tuple_element_t<0, decltype(curr)>, entt::entity>));
+        ASSERT_TRUE((std::is_same_v<std::tuple_element_t<1, decltype(curr)>, int &>));
+        ASSERT_TRUE((std::is_same_v<std::tuple_element_t<2, decltype(curr)>, char &>));
+        ++cnt;
+    }
+
+    ASSERT_EQ(cnt, std::size_t{6});
 
     cview.each([&cnt](auto, const int &, const char &) { --cnt; });
     cview.each([&cnt](const int &, const char &) { --cnt; });
 
+    for(auto curr: cview.each()) {
+        ASSERT_TRUE((std::is_same_v<std::tuple_element_t<0, decltype(curr)>, entt::entity>));
+        ASSERT_TRUE((std::is_same_v<std::tuple_element_t<1, decltype(curr)>, const int &>));
+        ASSERT_TRUE((std::is_same_v<std::tuple_element_t<2, decltype(curr)>, const char &>));
+        --cnt;
+    }
+
     ASSERT_EQ(cnt, std::size_t{0});
 }
 
@@ -407,6 +457,26 @@ TEST(MultiComponentView, EachWithSuggestedType) {
     registry.view<int, char>().each<int>([value = 0](const auto curr, const auto) mutable {
         ASSERT_EQ(curr, value++);
     });
+
+    registry.sort<int>([](const auto lhs, const auto rhs) {
+        return lhs > rhs;
+    });
+
+    auto value = registry.view<int, char>().size();
+
+    for(auto curr: registry.view<int, char>().each()) {
+        ASSERT_EQ(std::get<1>(curr), --value);
+    }
+
+    registry.sort<int>([](const auto lhs, const auto rhs) {
+        return lhs < rhs;
+    });
+
+    value = {};
+
+    for(auto curr: registry.view<int, char>().each<int>()) {
+        ASSERT_EQ(std::get<1>(curr), value++);
+    }
 }
 
 TEST(MultiComponentView, EachWithHoles) {
@@ -429,6 +499,12 @@ TEST(MultiComponentView, EachWithHoles) {
         ASSERT_EQ(c, '0');
         ASSERT_EQ(i, 0);
     });
+
+    for(auto curr: view.each()) {
+        ASSERT_EQ(std::get<0>(curr), e0);
+        ASSERT_EQ(std::get<1>(curr), '0');
+        ASSERT_EQ(std::get<2>(curr), 0);
+    }
 }
 
 TEST(MultiComponentView, ConstNonConstAndAllInBetween) {
@@ -453,6 +529,12 @@ TEST(MultiComponentView, ConstNonConstAndAllInBetween) {
         ASSERT_TRUE((std::is_same_v<decltype(i), int &>));
         ASSERT_TRUE((std::is_same_v<decltype(c), const char &>));
     });
+
+    for(auto curr: view.each()) {
+        ASSERT_TRUE((std::is_same_v<std::tuple_element_t<0, decltype(curr)>, entt::entity>));
+        ASSERT_TRUE((std::is_same_v<std::tuple_element_t<1, decltype(curr)>, int &>));
+        ASSERT_TRUE((std::is_same_v<std::tuple_element_t<2, decltype(curr)>, const char &>));
+    }
 }
 
 TEST(MultiComponentView, Find) {
@@ -545,7 +627,7 @@ TEST(MultiComponentView, ExcludedComponents) {
     }
 }
 
-TEST(MultiComponentView, EachWithEmptyTypes) {
+TEST(MultiComponentView, EmptyTypes) {
     entt::registry registry;
 
     const auto entity = registry.create();
@@ -562,27 +644,76 @@ TEST(MultiComponentView, EachWithEmptyTypes) {
         ASSERT_EQ(entity, entt);
     });
 
+    for(auto curr: registry.view<int, char, empty_type>().each()) {
+        ASSERT_EQ(std::tuple_size_v<decltype(curr)>, 3);
+        ASSERT_TRUE((std::is_same_v<std::tuple_element_t<0, decltype(curr)>, entt::entity>));
+        ASSERT_TRUE((std::is_same_v<std::tuple_element_t<1, decltype(curr)>, int &>));
+        ASSERT_TRUE((std::is_same_v<std::tuple_element_t<2, decltype(curr)>, char &>));
+        ASSERT_EQ(entity, std::get<0>(curr));
+    }
+
     registry.view<int, empty_type, char>().each([check = true](int, char) mutable {
         ASSERT_TRUE(check);
         check = false;
     });
 
+    for(auto curr: registry.view<int, empty_type, char>().each()) {
+        ASSERT_EQ(std::tuple_size_v<decltype(curr)>, 3);
+        ASSERT_TRUE((std::is_same_v<std::tuple_element_t<0, decltype(curr)>, entt::entity>));
+        ASSERT_TRUE((std::is_same_v<std::tuple_element_t<1, decltype(curr)>, int &>));
+        ASSERT_TRUE((std::is_same_v<std::tuple_element_t<2, decltype(curr)>, char &>));
+        ASSERT_EQ(entity, std::get<0>(curr));
+    }
+
     registry.view<empty_type, int, char>().each([entity](const auto entt, int, char) {
         ASSERT_EQ(entity, entt);
     });
 
+    for(auto curr: registry.view<empty_type, int, char>().each()) {
+        ASSERT_EQ(std::tuple_size_v<decltype(curr)>, 3);
+        ASSERT_TRUE((std::is_same_v<std::tuple_element_t<0, decltype(curr)>, entt::entity>));
+        ASSERT_TRUE((std::is_same_v<std::tuple_element_t<1, decltype(curr)>, int &>));
+        ASSERT_TRUE((std::is_same_v<std::tuple_element_t<2, decltype(curr)>, char &>));
+        ASSERT_EQ(entity, std::get<0>(curr));
+    }
+
     registry.view<empty_type, int, char>().each<empty_type>([entity](const auto entt, int, char) {
         ASSERT_EQ(entity, entt);
     });
 
+    for(auto curr: registry.view<empty_type, int, char>().each<empty_type>()) {
+        ASSERT_EQ(std::tuple_size_v<decltype(curr)>, 3);
+        ASSERT_TRUE((std::is_same_v<std::tuple_element_t<0, decltype(curr)>, entt::entity>));
+        ASSERT_TRUE((std::is_same_v<std::tuple_element_t<1, decltype(curr)>, int &>));
+        ASSERT_TRUE((std::is_same_v<std::tuple_element_t<2, decltype(curr)>, char &>));
+        ASSERT_EQ(entity, std::get<0>(curr));
+    }
+
     registry.view<int, empty_type, char>().each<empty_type>([check = true](int, char) mutable {
         ASSERT_TRUE(check);
         check = false;
     });
 
+    for(auto curr: registry.view<int, empty_type, char>().each<empty_type>()) {
+        ASSERT_EQ(std::tuple_size_v<decltype(curr)>, 3);
+        ASSERT_TRUE((std::is_same_v<std::tuple_element_t<0, decltype(curr)>, entt::entity>));
+        ASSERT_TRUE((std::is_same_v<std::tuple_element_t<1, decltype(curr)>, int &>));
+        ASSERT_TRUE((std::is_same_v<std::tuple_element_t<2, decltype(curr)>, char &>));
+        ASSERT_EQ(entity, std::get<0>(curr));
+    }
+
     registry.view<int, char, double>().each([entity](const auto entt, int, char, double) {
         ASSERT_EQ(entity, entt);
     });
+
+    for(auto curr: registry.view<int, char, double>().each()) {
+        ASSERT_EQ(std::tuple_size_v<decltype(curr)>, 4);
+        ASSERT_TRUE((std::is_same_v<std::tuple_element_t<0, decltype(curr)>, entt::entity>));
+        ASSERT_TRUE((std::is_same_v<std::tuple_element_t<1, decltype(curr)>, int &>));
+        ASSERT_TRUE((std::is_same_v<std::tuple_element_t<2, decltype(curr)>, char &>));
+        ASSERT_TRUE((std::is_same_v<std::tuple_element_t<3, decltype(curr)>, double &>));
+        ASSERT_EQ(entity, std::get<0>(curr));
+    }
 }
 
 TEST(MultiComponentView, FrontBack) {