Procházet zdrojové kódy

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

Michele Caini před 5 roky
rodič
revize
7cf2ec09b7
2 změnil soubory, kde provedl 489 přidání a 4 odebrání
  1. 353 0
      src/entt/entity/group.hpp
  2. 136 4
      test/entt/entity/group.cpp

+ 353 - 0
src/entt/entity/group.hpp

@@ -17,6 +17,325 @@
 namespace entt {
 namespace entt {
 
 
 
 
+/**
+ * @brief Group 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 group_range;
+
+
+/**
+ * @brief Non-owning group range.
+ *
+ * Iterable object to use to _visit_ a group.
+ *
+ * @sa group
+ *
+ * @tparam Entity A valid entity type (see entt_traits for more details).
+ * @tparam Exclude Types of components used to filter the group.
+ * @tparam Get Type of components observed by the group.
+ */
+template<typename Entity, typename... Exclude, typename... Get>
+class group_range<Entity, exclude_t<Exclude...>, get_t<Get...>> {
+    /*! @brief A group is allowed to create ranges. */
+    friend class basic_group<Entity, exclude_t<Exclude...>, get_t<Get...>>;
+
+    // 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;
+
+    class range_iterator {
+        friend class group_range<Entity, exclude_t<Exclude...>, entt::get_t<Get...>>;
+
+        using it_type = typename sparse_set<Entity>::iterator;
+        using pool_type = decltype(std::tuple_cat(std::declval<std::conditional_t<ENTT_IS_EMPTY(Get), std::tuple<>, std::tuple<pool_type<Get> *>>>()...));
+
+        range_iterator(it_type from, pool_type ref) ENTT_NOEXCEPT
+            : it{from},
+              pools{ref}
+        {}
+
+    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(Get), std::tuple<>, std::tuple<Get &>>>()...
+        ));
+        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 ++it, *this;
+        }
+
+        range_iterator operator++(int) ENTT_NOEXCEPT {
+            range_iterator orig = *this;
+            return ++(*this), orig;
+        }
+
+        [[nodiscard]] reference operator*() const ENTT_NOEXCEPT {
+            return std::apply([entt = *it](auto *... cpool) { return reference{entt, cpool->get(entt)...}; }, pools);
+        }
+
+        [[nodiscard]] bool operator==(const range_iterator &other) const ENTT_NOEXCEPT {
+            return other.it == it;
+        }
+
+        [[nodiscard]] bool operator!=(const range_iterator &other) const ENTT_NOEXCEPT {
+            return !(*this == other);
+        }
+
+    private:
+        it_type it;
+        pool_type pools;
+    };
+
+    group_range(const sparse_set<Entity> &ref, std::tuple<pool_type<Get> *...> gpools)
+        : handler{&ref},
+          pools{gpools}
+    {}
+
+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{handler->begin(), std::tuple_cat([](auto *cpool) {
+            if constexpr(ENTT_IS_EMPTY(typename std::decay_t<decltype(*cpool)>::object_type)) {
+                return std::make_tuple();
+            } else {
+                return std::make_tuple(cpool);
+            }
+        }(std::get<pool_type<Get> *>(pools))...)};
+    }
+
+    /**
+     * @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{handler->end(), std::tuple_cat([](auto *cpool) {
+            if constexpr(ENTT_IS_EMPTY(typename std::decay_t<decltype(*cpool)>::object_type)) {
+                return std::make_tuple();
+            } else {
+                return std::make_tuple(cpool);
+            }
+        }(std::get<pool_type<Get> *>(pools))...)};
+    }
+
+private:
+    const sparse_set<Entity> *handler;
+    std::tuple<pool_type<Get> *...> pools;
+};
+
+
+/**
+ * @brief Owning group range specialization.
+ *
+ * Iterable object to use to _visit_ a group.
+ *
+ * @sa group
+ *
+ * @tparam Entity A valid entity type (see entt_traits for more details).
+ * @tparam Exclude Types of components used to filter the group.
+ * @tparam Get Types of components observed by the group.
+ * @tparam Owned Types of components owned by the group.
+ */
+template<typename Entity, typename... Exclude, typename... Get, typename... Owned>
+class group_range<Entity, exclude_t<Exclude...>, get_t<Get...>, Owned...> {
+    /*! @brief A group is allowed to create ranges. */
+    friend class basic_group<Entity, exclude_t<Exclude...>, get_t<Get...>, Owned...>;
+
+    // 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;
+
+    class range_iterator {
+        friend class group_range<Entity, exclude_t<Exclude...>, entt::get_t<Get...>, Owned...>;
+
+        using it_type = typename sparse_set<Entity>::iterator;
+        using owned_type = decltype(std::tuple_cat(std::declval<std::conditional_t<ENTT_IS_EMPTY(Owned), std::tuple<>, std::tuple<decltype(std::declval<pool_type<Owned>>().begin())>>>()...));
+        using get_type = decltype(std::tuple_cat(std::declval<std::conditional_t<ENTT_IS_EMPTY(Get), std::tuple<>, std::tuple<pool_type<Get> *>>>()...));
+
+        range_iterator(it_type from, owned_type oref, get_type gref) ENTT_NOEXCEPT
+            : it{from},
+              owned{oref},
+              get{gref}
+        {}
+
+    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(Owned), std::tuple<>, std::tuple<Owned &>>>()...,
+            std::declval<std::conditional_t<ENTT_IS_EMPTY(Get), std::tuple<>, std::tuple<Get &>>>()...
+        ));
+        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 ++it, std::apply([](auto &&... curr) { (++curr, ...); }, owned), *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(*it),
+                std::apply([](auto &&... curr) { return std::forward_as_tuple(*curr...); }, owned),
+                std::apply([entt = *it](auto &&... curr) { return std::forward_as_tuple(curr->get(entt)...); }, get)
+            );
+        }
+
+        [[nodiscard]] bool operator==(const range_iterator &other) const ENTT_NOEXCEPT {
+            return other.it == it;
+        }
+
+        [[nodiscard]] bool operator!=(const range_iterator &other) const ENTT_NOEXCEPT {
+            return !(*this == other);
+        }
+
+    private:
+        it_type it;
+        owned_type owned;
+        get_type get;
+    };
+
+    group_range(std::tuple<pool_type<Owned> *..., pool_type<Get> *...> cpools, const std::size_t &extent)
+        : pools{cpools},
+          length{&extent}
+    {}
+
+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{
+            std::get<0>(pools)->sparse_set<Entity>::end() - *length,
+            std::tuple_cat([this](auto *cpool) {
+                if constexpr(ENTT_IS_EMPTY(typename std::decay_t<decltype(*cpool)>::object_type)) {
+                    return std::make_tuple();
+                } else {
+                    return std::make_tuple(cpool->end() - *length);
+                }
+            }(std::get<pool_type<Owned> *>(pools))...),
+            std::tuple_cat([](auto *cpool) {
+                if constexpr(ENTT_IS_EMPTY(typename std::decay_t<decltype(*cpool)>::object_type)) {
+                    return std::make_tuple();
+                } else {
+                    return std::make_tuple(cpool);
+                }
+            }(std::get<pool_type<Get> *>(pools))...)
+        };
+    }
+
+    /**
+     * @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{
+            std::get<0>(pools)->sparse_set<Entity>::end(),
+            std::tuple_cat([](auto *cpool) {
+                if constexpr(ENTT_IS_EMPTY(typename std::decay_t<decltype(*cpool)>::object_type)) {
+                    return std::make_tuple();
+                } else {
+                    return std::make_tuple(cpool->end());
+                }
+            }(std::get<pool_type<Owned> *>(pools))...),
+            std::tuple_cat([](auto *cpool) {
+                if constexpr(ENTT_IS_EMPTY(typename std::decay_t<decltype(*cpool)>::object_type)) {
+                    return std::make_tuple();
+                } else {
+                    return std::make_tuple(cpool);
+                }
+            }(std::get<pool_type<Get> *>(pools))...)
+        };
+    }
+
+private:
+    const std::tuple<pool_type<Owned> *..., pool_type<Get> *...> pools;
+    const std::size_t *length;
+};
+
+
 /**
 /**
  * @brief Group.
  * @brief Group.
  *
  *
@@ -346,6 +665,23 @@ public:
         traverse(std::move(func), get_type_list{});
         traverse(std::move(func), get_type_list{});
     }
     }
 
 
+    /**
+     * @brief Returns an iterable object to use to _visit_ the group.
+     *
+     * 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 group.
+     */
+    group_range<entity_type, exclude_t<Exclude...>, get_t<Get...>> each() const {
+        return { *handler, pools };
+    }
+
     /**
     /**
      * @brief Sort a group according to the given comparison function.
      * @brief Sort a group according to the given comparison function.
      *
      *
@@ -759,6 +1095,23 @@ public:
         traverse(std::move(func), owned_type_list{}, get_type_list{});
         traverse(std::move(func), owned_type_list{}, get_type_list{});
     }
     }
 
 
+    /**
+     * @brief Returns an iterable object to use to _visit_ the group.
+     *
+     * 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 group.
+     */
+    group_range<entity_type, exclude_t<Exclude...>, get_t<Get...>, Owned...> each() const {
+        return { pools, *length };
+    }
+
     /**
     /**
      * @brief Sort a group according to the given comparison function.
      * @brief Sort a group according to the given comparison function.
      *
      *

+ 136 - 4
test/entt/entity/group.cpp

@@ -158,11 +158,25 @@ TEST(NonOwningGroup, Each) {
     group.each([&cnt](auto, int &, char &) { ++cnt; });
     group.each([&cnt](auto, int &, char &) { ++cnt; });
     group.each([&cnt](int &, char &) { ++cnt; });
     group.each([&cnt](int &, char &) { ++cnt; });
 
 
-    ASSERT_EQ(cnt, std::size_t{4});
+    for(auto curr: group.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});
 
 
     cgroup.each([&cnt](auto, const int &, const char &) { --cnt; });
     cgroup.each([&cnt](auto, const int &, const char &) { --cnt; });
     cgroup.each([&cnt](const int &, const char &) { --cnt; });
     cgroup.each([&cnt](const int &, const char &) { --cnt; });
 
 
+    for(auto curr: cgroup.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});
     ASSERT_EQ(cnt, std::size_t{0});
 }
 }
 
 
@@ -300,6 +314,12 @@ TEST(NonOwningGroup, IndexRebuiltOnDestroy) {
         ASSERT_EQ(ivalue, 1);
         ASSERT_EQ(ivalue, 1);
         ASSERT_EQ(uivalue, 1u);
         ASSERT_EQ(uivalue, 1u);
     });
     });
+
+    for(auto curr: group.each()) {
+        ASSERT_EQ(std::get<0>(curr), e1);
+        ASSERT_EQ(std::get<1>(curr), 1);
+        ASSERT_EQ(std::get<2>(curr), 1u);
+    }
 }
 }
 
 
 TEST(NonOwningGroup, ConstNonConstAndAllInBetween) {
 TEST(NonOwningGroup, ConstNonConstAndAllInBetween) {
@@ -324,6 +344,12 @@ TEST(NonOwningGroup, ConstNonConstAndAllInBetween) {
         ASSERT_TRUE((std::is_same_v<decltype(i), int &>));
         ASSERT_TRUE((std::is_same_v<decltype(i), int &>));
         ASSERT_TRUE((std::is_same_v<decltype(c), const char &>));
         ASSERT_TRUE((std::is_same_v<decltype(c), const char &>));
     });
     });
+
+    for(auto curr: group.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(NonOwningGroup, Find) {
 TEST(NonOwningGroup, Find) {
@@ -441,6 +467,13 @@ TEST(NonOwningGroup, EmptyAndNonEmptyTypes) {
         ASSERT_TRUE(entity == e0 || entity == e1);
         ASSERT_TRUE(entity == e0 || entity == e1);
     });
     });
 
 
+    for(auto curr: group.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_TRUE(std::get<0>(curr) == e0 || std::get<0>(curr) == e1);
+    }
+
     ASSERT_EQ(group.size(), typename decltype(group)::size_type{2});
     ASSERT_EQ(group.size(), typename decltype(group)::size_type{2});
 }
 }
 
 
@@ -462,7 +495,7 @@ TEST(NonOwningGroup, TrackEntitiesOnComponentDestruction) {
     ASSERT_FALSE(cgroup.empty());
     ASSERT_FALSE(cgroup.empty());
 }
 }
 
 
-TEST(NonOwningGroup, EachWithEmptyTypes) {
+TEST(NonOwningGroup, EmptyTypes) {
     entt::registry registry;
     entt::registry registry;
     const auto entity = registry.create();
     const auto entity = registry.create();
 
 
@@ -474,16 +507,49 @@ TEST(NonOwningGroup, EachWithEmptyTypes) {
         ASSERT_EQ(entity, entt);
         ASSERT_EQ(entity, entt);
     });
     });
 
 
+    for(auto curr: registry.group(entt::get<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.group(entt::get<int, empty_type, char>).each([check = true](int, char) mutable {
     registry.group(entt::get<int, empty_type, char>).each([check = true](int, char) mutable {
         ASSERT_TRUE(check);
         ASSERT_TRUE(check);
         check = false;
         check = false;
     });
     });
 
 
+    for(auto curr: registry.group(entt::get<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.group(entt::get<empty_type, int, char>).each([entity](const auto entt, int, char) {
     registry.group(entt::get<empty_type, int, char>).each([entity](const auto entt, int, char) {
         ASSERT_EQ(entity, entt);
         ASSERT_EQ(entity, entt);
     });
     });
 
 
+    for(auto curr: registry.group(entt::get<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.group(entt::get<int, char, double>).each([](const auto, int, char, double) { FAIL(); });
     registry.group(entt::get<int, char, double>).each([](const auto, int, char, double) { FAIL(); });
+
+    for(auto curr: registry.group(entt::get<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(NonOwningGroup, FrontBack) {
 TEST(NonOwningGroup, FrontBack) {
@@ -661,11 +727,25 @@ TEST(OwningGroup, Each) {
     group.each([&cnt](auto, int &, char &) { ++cnt; });
     group.each([&cnt](auto, int &, char &) { ++cnt; });
     group.each([&cnt](int &, char &) { ++cnt; });
     group.each([&cnt](int &, char &) { ++cnt; });
 
 
-    ASSERT_EQ(cnt, std::size_t{4});
+    for(auto curr: group.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});
 
 
     cgroup.each([&cnt](auto, const int &, const char &) { --cnt; });
     cgroup.each([&cnt](auto, const int &, const char &) { --cnt; });
     cgroup.each([&cnt](const int &, const char &) { --cnt; });
     cgroup.each([&cnt](const int &, const char &) { --cnt; });
 
 
+    for(auto curr: cgroup.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});
     ASSERT_EQ(cnt, std::size_t{0});
 }
 }
 
 
@@ -887,6 +967,12 @@ TEST(OwningGroup, IndexRebuiltOnDestroy) {
         ASSERT_EQ(ivalue, 1);
         ASSERT_EQ(ivalue, 1);
         ASSERT_EQ(uivalue, 1u);
         ASSERT_EQ(uivalue, 1u);
     });
     });
+
+    for(auto curr: group.each()) {
+        ASSERT_EQ(std::get<0>(curr), e1);
+        ASSERT_EQ(std::get<1>(curr), 1);
+        ASSERT_EQ(std::get<2>(curr), 1u);
+    }
 }
 }
 
 
 TEST(OwningGroup, ConstNonConstAndAllInBetween) {
 TEST(OwningGroup, ConstNonConstAndAllInBetween) {
@@ -919,6 +1005,12 @@ TEST(OwningGroup, ConstNonConstAndAllInBetween) {
         ASSERT_TRUE((std::is_same_v<decltype(d), double &>));
         ASSERT_TRUE((std::is_same_v<decltype(d), double &>));
         ASSERT_TRUE((std::is_same_v<decltype(f), const float &>));
         ASSERT_TRUE((std::is_same_v<decltype(f), const float &>));
     });
     });
+
+    for(auto curr: group.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(OwningGroup, Find) {
 TEST(OwningGroup, Find) {
@@ -1036,6 +1128,13 @@ TEST(OwningGroup, EmptyAndNonEmptyTypes) {
         ASSERT_TRUE(entity == e0 || entity == e1);
         ASSERT_TRUE(entity == e0 || entity == e1);
     });
     });
 
 
+    for(auto curr: group.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_TRUE(std::get<0>(curr) == e0 || std::get<0>(curr) == e1);
+    }
+
     ASSERT_EQ(group.size(), typename decltype(group)::size_type{2});
     ASSERT_EQ(group.size(), typename decltype(group)::size_type{2});
 }
 }
 
 
@@ -1057,7 +1156,7 @@ TEST(OwningGroup, TrackEntitiesOnComponentDestruction) {
     ASSERT_FALSE(cgroup.empty());
     ASSERT_FALSE(cgroup.empty());
 }
 }
 
 
-TEST(OwningGroup, EachWithEmptyTypes) {
+TEST(OwningGroup, EmptyTypes) {
     entt::registry registry;
     entt::registry registry;
     const auto entity = registry.create();
     const auto entity = registry.create();
 
 
@@ -1069,16 +1168,49 @@ TEST(OwningGroup, EachWithEmptyTypes) {
         ASSERT_EQ(entity, entt);
         ASSERT_EQ(entity, entt);
     });
     });
 
 
+    for(auto curr: registry.group<int>(entt::get<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.group<char>(entt::get<empty_type, int>).each([check = true](int, char) mutable {
     registry.group<char>(entt::get<empty_type, int>).each([check = true](int, char) mutable {
         ASSERT_TRUE(check);
         ASSERT_TRUE(check);
         check = false;
         check = false;
     });
     });
 
 
+    for(auto curr: registry.group<char>(entt::get<empty_type, int>).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)>, char &>));
+        ASSERT_TRUE((std::is_same_v<std::tuple_element_t<2, decltype(curr)>, int &>));
+        ASSERT_EQ(entity, std::get<0>(curr));
+    }
+
     registry.group<empty_type>(entt::get<int, char>).each([entity](const auto entt, int, char) {
     registry.group<empty_type>(entt::get<int, char>).each([entity](const auto entt, int, char) {
         ASSERT_EQ(entity, entt);
         ASSERT_EQ(entity, entt);
     });
     });
 
 
+    for(auto curr: registry.group<empty_type>(entt::get<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.group<double>(entt::get<int, char>).each([](const auto, double, int, char) { FAIL(); });
     registry.group<double>(entt::get<int, char>).each([](const auto, double, int, char) { FAIL(); });
+
+    for(auto curr: registry.group<double>(entt::get<int, char>).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)>, double &>));
+        ASSERT_TRUE((std::is_same_v<std::tuple_element_t<2, decltype(curr)>, int &>));
+        ASSERT_TRUE((std::is_same_v<std::tuple_element_t<3, decltype(curr)>, char &>));
+        ASSERT_EQ(entity, std::get<0>(curr));
+    }
 }
 }
 
 
 TEST(OwningGroup, FrontBack) {
 TEST(OwningGroup, FrontBack) {