Sfoglia il codice sorgente

container: added dense_hash_set

Michele Caini 4 anni fa
parent
commit
16e3cfc589

+ 1 - 0
CMakeLists.txt

@@ -121,6 +121,7 @@ if(ENTT_INCLUDE_HEADERS)
             $<BUILD_INTERFACE:${EnTT_SOURCE_DIR}/src/entt/config/config.h>
             $<BUILD_INTERFACE:${EnTT_SOURCE_DIR}/src/entt/config/config.h>
             $<BUILD_INTERFACE:${EnTT_SOURCE_DIR}/src/entt/config/version.h>
             $<BUILD_INTERFACE:${EnTT_SOURCE_DIR}/src/entt/config/version.h>
             $<BUILD_INTERFACE:${EnTT_SOURCE_DIR}/src/entt/container/dense_hash_map.hpp>
             $<BUILD_INTERFACE:${EnTT_SOURCE_DIR}/src/entt/container/dense_hash_map.hpp>
+            $<BUILD_INTERFACE:${EnTT_SOURCE_DIR}/src/entt/container/dense_hash_set.hpp>
             $<BUILD_INTERFACE:${EnTT_SOURCE_DIR}/src/entt/container/fwd.hpp>
             $<BUILD_INTERFACE:${EnTT_SOURCE_DIR}/src/entt/container/fwd.hpp>
             $<BUILD_INTERFACE:${EnTT_SOURCE_DIR}/src/entt/core/algorithm.hpp>
             $<BUILD_INTERFACE:${EnTT_SOURCE_DIR}/src/entt/core/algorithm.hpp>
             $<BUILD_INTERFACE:${EnTT_SOURCE_DIR}/src/entt/core/any.hpp>
             $<BUILD_INTERFACE:${EnTT_SOURCE_DIR}/src/entt/core/any.hpp>

+ 13 - 0
docs/md/container.md

@@ -8,6 +8,7 @@
 * [Introduction](#introduction)
 * [Introduction](#introduction)
 * [Containers](#containers)
 * [Containers](#containers)
   * [Dense hash map](#dense-hash-map)
   * [Dense hash map](#dense-hash-map)
+  * [Dense hash set](#dense-hash-set)
 
 
 <!--
 <!--
 @endcond TURN_OFF_DOXYGEN
 @endcond TURN_OFF_DOXYGEN
@@ -41,3 +42,15 @@ implicit list within the packed array itself.
 The interface is in all respects similar to its counterpart in the standard
 The interface is in all respects similar to its counterpart in the standard
 library, that is, `std::unordered_map`.<br/>
 library, that is, `std::unordered_map`.<br/>
 Therefore, there is no need to go into the API description.
 Therefore, there is no need to go into the API description.
+
+## Dense hash set
+
+The dense hash set made available in `EnTT` is a set that aims to return a
+packed array of elements, so as to reduce the number of jumps in memory during
+the iteration.<br/>
+The implementation is based on _sparse sets_ and each bucket is identified by an
+implicit list within the packed array itself.
+
+The interface is in all respects similar to its counterpart in the standard
+library, that is, `std::unordered_set`.<br/>
+Therefore, there is no need to go into the API description.

+ 846 - 0
src/entt/container/dense_hash_set.hpp

@@ -0,0 +1,846 @@
+#ifndef ENTT_CONTAINER_DENSE_HASH_SET_HPP
+#define ENTT_CONTAINER_DENSE_HASH_SET_HPP
+
+#include <algorithm>
+#include <cmath>
+#include <cstddef>
+#include <functional>
+#include <iterator>
+#include <limits>
+#include <memory>
+#include <tuple>
+#include <type_traits>
+#include <utility>
+#include <vector>
+#include "../config/config.h"
+#include "../core/compressed_pair.hpp"
+#include "../core/memory.hpp"
+#include "../core/type_traits.hpp"
+#include "fwd.hpp"
+
+namespace entt {
+
+/**
+ * @cond TURN_OFF_DOXYGEN
+ * Internal details not to be documented.
+ */
+
+namespace internal {
+
+template<typename Type>
+struct dense_hash_set_node final {
+    template<typename... Args>
+    dense_hash_set_node(const std::size_t pos, Args &&...args)
+        : next{pos},
+          element{std::forward<Args>(args)...} {}
+
+    std::size_t next;
+    Type element;
+};
+
+template<typename It>
+class dense_hash_set_iterator {
+    friend dense_hash_set_iterator<const std::remove_pointer_t<It> *>;
+
+    using iterator_traits = std::iterator_traits<decltype(std::addressof(std::as_const(std::declval<It>()->element)))>;
+
+public:
+    using iterator_type = It;
+    using value_type = typename iterator_traits::value_type;
+    using pointer = typename iterator_traits::pointer;
+    using reference = typename iterator_traits::reference;
+    using difference_type = typename iterator_traits::difference_type;
+    using iterator_category = std::random_access_iterator_tag;
+
+    dense_hash_set_iterator() ENTT_NOEXCEPT = default;
+
+    dense_hash_set_iterator(const iterator_type iter) ENTT_NOEXCEPT
+        : it{iter} {}
+
+    template<bool Const = std::is_const_v<std::remove_pointer_t<iterator_type>>, typename = std::enable_if_t<Const>>
+    dense_hash_set_iterator(const dense_hash_set_iterator<std::remove_const_t<std::remove_pointer_t<iterator_type>> *> &other)
+        : it{other.it} {}
+
+    dense_hash_set_iterator &operator++() ENTT_NOEXCEPT {
+        return ++it, *this;
+    }
+
+    dense_hash_set_iterator operator++(int) ENTT_NOEXCEPT {
+        dense_hash_set_iterator orig = *this;
+        return ++(*this), orig;
+    }
+
+    dense_hash_set_iterator &operator--() ENTT_NOEXCEPT {
+        return --it, *this;
+    }
+
+    dense_hash_set_iterator operator--(int) ENTT_NOEXCEPT {
+        dense_hash_set_iterator orig = *this;
+        return operator--(), orig;
+    }
+
+    dense_hash_set_iterator &operator+=(const difference_type value) ENTT_NOEXCEPT {
+        it += value;
+        return *this;
+    }
+
+    dense_hash_set_iterator operator+(const difference_type value) const ENTT_NOEXCEPT {
+        dense_hash_set_iterator copy = *this;
+        return (copy += value);
+    }
+
+    dense_hash_set_iterator &operator-=(const difference_type value) ENTT_NOEXCEPT {
+        return (*this += -value);
+    }
+
+    dense_hash_set_iterator operator-(const difference_type value) const ENTT_NOEXCEPT {
+        return (*this + -value);
+    }
+
+    [[nodiscard]] reference operator[](const difference_type value) const {
+        return it->element;
+    }
+
+    [[nodiscard]] pointer operator->() const {
+        return std::addressof(it->element);
+    }
+
+    [[nodiscard]] reference operator*() const {
+        return *operator->();
+    }
+
+    [[nodiscard]] iterator_type base() const ENTT_NOEXCEPT {
+        return it;
+    }
+
+private:
+    iterator_type it;
+};
+
+template<typename ILhs, typename IRhs>
+[[nodiscard]] auto operator-(const dense_hash_set_iterator<ILhs> &lhs, const dense_hash_set_iterator<IRhs> &rhs) ENTT_NOEXCEPT {
+    return lhs.base() - rhs.base();
+}
+
+template<typename ILhs, typename IRhs>
+[[nodiscard]] bool operator==(const dense_hash_set_iterator<ILhs> &lhs, const dense_hash_set_iterator<IRhs> &rhs) ENTT_NOEXCEPT {
+    return lhs.base() == rhs.base();
+}
+
+template<typename ILhs, typename IRhs>
+[[nodiscard]] bool operator!=(const dense_hash_set_iterator<ILhs> &lhs, const dense_hash_set_iterator<IRhs> &rhs) ENTT_NOEXCEPT {
+    return !(lhs == rhs);
+}
+
+template<typename ILhs, typename IRhs>
+[[nodiscard]] bool operator<(const dense_hash_set_iterator<ILhs> &lhs, const dense_hash_set_iterator<IRhs> &rhs) ENTT_NOEXCEPT {
+    return lhs.base() < rhs.base();
+}
+
+template<typename ILhs, typename IRhs>
+[[nodiscard]] bool operator>(const dense_hash_set_iterator<ILhs> &lhs, const dense_hash_set_iterator<IRhs> &rhs) ENTT_NOEXCEPT {
+    return lhs.base() > rhs.base();
+}
+
+template<typename ILhs, typename IRhs>
+[[nodiscard]] bool operator<=(const dense_hash_set_iterator<ILhs> &lhs, const dense_hash_set_iterator<IRhs> &rhs) ENTT_NOEXCEPT {
+    return !(lhs > rhs);
+}
+
+template<typename ILhs, typename IRhs>
+[[nodiscard]] bool operator>=(const dense_hash_set_iterator<ILhs> &lhs, const dense_hash_set_iterator<IRhs> &rhs) ENTT_NOEXCEPT {
+    return !(lhs < rhs);
+}
+
+template<typename It>
+class dense_hash_set_local_iterator {
+    friend dense_hash_set_local_iterator<const std::remove_pointer_t<It> *>;
+
+    using iterator_traits = std::iterator_traits<decltype(std::addressof(std::as_const(std::declval<It>()->element)))>;
+
+public:
+    using iterator_type = It;
+    using value_type = typename iterator_traits::value_type;
+    using pointer = typename iterator_traits::pointer;
+    using reference = typename iterator_traits::reference;
+    using difference_type = typename iterator_traits::difference_type;
+    using iterator_category = std::forward_iterator_tag;
+
+    dense_hash_set_local_iterator() ENTT_NOEXCEPT = default;
+
+    dense_hash_set_local_iterator(iterator_type iter, const std::size_t pos) ENTT_NOEXCEPT
+        : it{iter},
+          curr{pos} {}
+
+    template<bool Const = std::is_const_v<std::remove_pointer_t<iterator_type>>, typename = std::enable_if_t<Const>>
+    dense_hash_set_local_iterator(const dense_hash_set_local_iterator<std::remove_const_t<std::remove_pointer_t<iterator_type>> *> &other)
+        : it{other.it},
+          curr{other.curr} {}
+
+    dense_hash_set_local_iterator &operator++() ENTT_NOEXCEPT {
+        return curr = it[curr].next, *this;
+    }
+
+    dense_hash_set_local_iterator operator++(int) ENTT_NOEXCEPT {
+        dense_hash_set_local_iterator orig = *this;
+        return ++(*this), orig;
+    }
+
+    [[nodiscard]] pointer operator->() const {
+        return std::addressof(it[curr].element);
+    }
+
+    [[nodiscard]] reference operator*() const {
+        return *operator->();
+    }
+
+    [[nodiscard]] iterator_type base() const ENTT_NOEXCEPT {
+        return (it + curr);
+    }
+
+private:
+    iterator_type it;
+    std::size_t curr;
+};
+
+template<typename ILhs, typename IRhs>
+[[nodiscard]] bool operator==(const dense_hash_set_local_iterator<ILhs> &lhs, const dense_hash_set_local_iterator<IRhs> &rhs) ENTT_NOEXCEPT {
+    return lhs.base() == rhs.base();
+}
+
+template<typename ILhs, typename IRhs>
+[[nodiscard]] bool operator!=(const dense_hash_set_local_iterator<ILhs> &lhs, const dense_hash_set_local_iterator<IRhs> &rhs) ENTT_NOEXCEPT {
+    return !(lhs == rhs);
+}
+
+} // namespace internal
+
+/**
+ * Internal details not to be documented.
+ * @endcond
+ */
+
+/**
+ * @brief Associative container for unique objects of a given type.
+ *
+ * Internally, elements are organized into buckets. Which bucket an element is
+ * placed into depends entirely on its hash. Elements with the same hash code
+ * appear in the same bucket.
+ *
+ * @tparam Type Value type of the associative container.
+ * @tparam Hash Type of function to use to hash the values.
+ * @tparam KeyEqual Type of function to use to compare the values for equality.
+ * @tparam Allocator Type of allocator used to manage memory and elements.
+ */
+template<typename Type, typename Hash, typename KeyEqual, typename Allocator>
+class dense_hash_set final {
+    static constexpr float default_threshold = 0.875f;
+    static constexpr std::size_t minimum_capacity = 8u;
+
+    using allocator_traits = std::allocator_traits<Allocator>;
+    using alloc = typename allocator_traits::template rebind_alloc<Type>;
+    using alloc_traits = typename std::allocator_traits<alloc>;
+
+    using node_type = internal::dense_hash_set_node<Type>;
+    using sparse_container_type = std::vector<std::size_t, typename alloc_traits::template rebind_alloc<std::size_t>>;
+    using packed_container_type = std::vector<node_type, typename alloc_traits::template rebind_alloc<node_type>>;
+
+    [[nodiscard]] std::size_t hash_to_bucket(const std::size_t hash) const ENTT_NOEXCEPT {
+        return fast_mod(hash, bucket_count());
+    }
+
+    template<typename Other>
+    [[nodiscard]] auto constrained_find(const Other &value, std::size_t bucket) {
+        for(auto it = begin(bucket), last = end(bucket); it != last; ++it) {
+            if(packed.second()(*it, value)) {
+                return iterator{it.base()};
+            }
+        }
+
+        return end();
+    }
+
+    template<typename Other>
+    [[nodiscard]] auto constrained_find(const Other &value, std::size_t bucket) const {
+        for(auto it = begin(bucket), last = end(bucket); it != last; ++it) {
+            if(packed.second()(*it, value)) {
+                return const_iterator{it.base()};
+            }
+        }
+
+        return cend();
+    }
+
+    template<typename Arg>
+    [[nodiscard]] auto get_or_emplace(Arg &&arg) {
+        const auto hash = sparse.second()(arg);
+        auto index = hash_to_bucket(hash);
+
+        if(auto it = constrained_find(arg, index); it != end()) {
+            return std::make_pair(it, false);
+        }
+
+        if(const auto count = size() + 1u; count > (bucket_count() * max_load_factor())) {
+            rehash(bucket_count() * 2u);
+            index = hash_to_bucket(hash);
+        }
+
+        packed.first().emplace_back(sparse.first()[index], std::forward<Arg>(arg));
+        // update goes after emplace to enforce exception guarantees
+        sparse.first()[index] = size() - 1u;
+
+        return std::make_pair(--end(), true);
+    }
+
+    template<typename Other>
+    bool do_erase(const Other &value) {
+        for(size_type *curr = sparse.first().data() + bucket(value); *curr != std::numeric_limits<size_type>::max(); curr = &packed.first()[*curr].next) {
+            if(packed.second()(packed.first()[*curr].element, value)) {
+                const auto index = *curr;
+                *curr = packed.first()[*curr].next;
+                move_and_pop(index);
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    void move_and_pop(const std::size_t pos) {
+        if(const auto last = size() - 1u; pos != last) {
+            size_type *curr = sparse.first().data() + bucket(packed.first().back().element);
+            for(; *curr != last; curr = &packed.first()[*curr].next) {}
+            *curr = pos;
+
+            // basic exception guarantees when value type has a throwing move constructor
+            packed.first()[pos] = std::move(packed.first().back());
+        }
+
+        packed.first().pop_back();
+    }
+
+public:
+    /*! @brief Key type of the container. */
+    using key_type = Type;
+    /*! @brief Value type of the container. */
+    using value_type = Type;
+    /*! @brief Unsigned integer type. */
+    using size_type = std::size_t;
+    /*! @brief Type of function to use to hash the elements. */
+    using hasher = Hash;
+    /*! @brief Type of function to use to compare the elements for equality. */
+    using key_equal = KeyEqual;
+    /*! @brief Allocator type. */
+    using allocator_type = Allocator;
+    /*! @brief Random access iterator type. */
+    using iterator = internal::dense_hash_set_iterator<typename packed_container_type::pointer>;
+    /*! @brief Constant random access iterator type. */
+    using const_iterator = internal::dense_hash_set_iterator<typename packed_container_type::const_pointer>;
+    /*! @brief Forward iterator type. */
+    using local_iterator = internal::dense_hash_set_local_iterator<typename packed_container_type::pointer>;
+    /*! @brief Constant forward iterator type. */
+    using const_local_iterator = internal::dense_hash_set_local_iterator<typename packed_container_type::const_pointer>;
+
+    /*! @brief Default constructor. */
+    dense_hash_set()
+        : dense_hash_set(minimum_capacity) {}
+
+    /**
+     * @brief Constructs an empty container with a given allocator.
+     * @param allocator The allocator to use.
+     */
+    explicit dense_hash_set(const allocator_type &allocator)
+        : dense_hash_set{minimum_capacity, hasher{}, key_equal{}, allocator} {}
+
+    /**
+     * @brief Constructs an empty container with a given allocator and user
+     * supplied minimal number of buckets.
+     * @param bucket_count Minimal number of buckets.
+     * @param allocator The allocator to use.
+     */
+    dense_hash_set(const size_type bucket_count, const allocator_type &allocator)
+        : dense_hash_set{bucket_count, hasher{}, key_equal{}, allocator} {}
+
+    /**
+     * @brief Constructs an empty container with a given allocator, hash
+     * function and user supplied minimal number of buckets.
+     * @param bucket_count Minimal number of buckets.
+     * @param hash Hash function to use.
+     * @param allocator The allocator to use.
+     */
+    dense_hash_set(const size_type bucket_count, const hasher &hash, const allocator_type &allocator)
+        : dense_hash_set{bucket_count, hash, key_equal{}, allocator} {}
+
+    /**
+     * @brief Constructs an empty container with a given allocator, hash
+     * function, compare function and user supplied minimal number of buckets.
+     * @param bucket_count Minimal number of buckets.
+     * @param hash Hash function to use.
+     * @param equal Compare function to use.
+     * @param allocator The allocator to use.
+     */
+    explicit dense_hash_set(const size_type bucket_count, const hasher &hash = hasher{}, const key_equal &equal = key_equal{}, const allocator_type &allocator = allocator_type())
+        : sparse{allocator, hash},
+          packed{allocator, equal},
+          threshold{default_threshold} {
+        rehash(bucket_count);
+    }
+
+    /**
+     * @brief Copy constructor.
+     * @param other The instance to copy from.
+     */
+    dense_hash_set(const dense_hash_set &other)
+        : dense_hash_set{other, alloc_traits::select_on_container_copy_construction(other.get_allocator())} {}
+
+    /**
+     * @brief Allocator-extended copy constructor.
+     * @param other The instance to copy from.
+     * @param allocator The allocator to use.
+     */
+    dense_hash_set(const dense_hash_set &other, const allocator_type &allocator)
+        : sparse{sparse_container_type{other.sparse.first(), allocator}, other.sparse.second()},
+          // cannot copy the container directly due to a nasty issue of apple clang :(
+          packed{packed_container_type{other.packed.first().begin(), other.packed.first().end(), allocator}, other.packed.second()},
+          threshold{other.threshold} {
+    }
+
+    /**
+     * @brief Default move constructor.
+     * @param other The instance to move from.
+     */
+    dense_hash_set(dense_hash_set &&other) ENTT_NOEXCEPT = default;
+
+    /**
+     * @brief Allocator-extended move constructor.
+     * @param other The instance to move from.
+     * @param allocator The allocator to use.
+     */
+    dense_hash_set(dense_hash_set &&other, const allocator_type &allocator) ENTT_NOEXCEPT
+        : sparse{sparse_container_type{std::move(other.sparse.first()), allocator}, std::move(other.sparse.second())},
+          // cannot move the container directly due to a nasty issue of apple clang :(
+          packed{packed_container_type{std::make_move_iterator(other.packed.first().begin()), std::make_move_iterator(other.packed.first().end()), allocator}, std::move(other.packed.second())},
+          threshold{other.threshold} {}
+
+    /*! @brief Default destructor. */
+    ~dense_hash_set() = default;
+
+    /**
+     * @brief Copy assignment operator.
+     * @param other The instance to copy from.
+     * @return This container.
+     */
+    dense_hash_set &operator=(const dense_hash_set &other) {
+        threshold = other.threshold;
+        sparse.first().clear();
+        packed.first().clear();
+        rehash(other.bucket_count());
+        packed.first().reserve(other.packed.first().size());
+        insert(other.cbegin(), other.cend());
+        return *this;
+    }
+
+    /**
+     * @brief Default move assignment operator.
+     * @param other The instance to move from.
+     * @return This container.
+     */
+    dense_hash_set &operator=(dense_hash_set &&other) ENTT_NOEXCEPT = default;
+
+    /**
+     * @brief Returns the associated allocator.
+     * @return The associated allocator.
+     */
+    [[nodiscard]] constexpr allocator_type get_allocator() const ENTT_NOEXCEPT {
+        return sparse.first().get_allocator();
+    }
+
+    /**
+     * @brief Returns an iterator to the beginning.
+     *
+     * The returned iterator points to the first instance of the internal array.
+     * If the array is empty, the returned iterator will be equal to `end()`.
+     *
+     * @return An iterator to the first instance of the internal array.
+     */
+    [[nodiscard]] const_iterator cbegin() const ENTT_NOEXCEPT {
+        return packed.first().data();
+    }
+
+    /*! @copydoc cbegin */
+    [[nodiscard]] const_iterator begin() const ENTT_NOEXCEPT {
+        return cbegin();
+    }
+
+    /*! @copydoc begin */
+    [[nodiscard]] iterator begin() ENTT_NOEXCEPT {
+        return packed.first().data();
+    }
+
+    /**
+     * @brief Returns an iterator to the end.
+     *
+     * The returned iterator points to the element following the last instance
+     * of the internal array. Attempting to dereference the returned iterator
+     * results in undefined behavior.
+     *
+     * @return An iterator to the element following the last instance of the
+     * internal array.
+     */
+    [[nodiscard]] const_iterator cend() const ENTT_NOEXCEPT {
+        return packed.first().data() + size();
+    }
+
+    /*! @copydoc cend */
+    [[nodiscard]] const_iterator end() const ENTT_NOEXCEPT {
+        return cend();
+    }
+
+    /*! @copydoc end */
+    [[nodiscard]] iterator end() ENTT_NOEXCEPT {
+        return packed.first().data() + size();
+    }
+
+    /**
+     * @brief Checks whether a container is empty.
+     * @return True if the container is empty, false otherwise.
+     */
+    [[nodiscard]] bool empty() const ENTT_NOEXCEPT {
+        return packed.first().empty();
+    }
+
+    /**
+     * @brief Returns the number of elements in a container.
+     * @return Number of elements in a container.
+     */
+    [[nodiscard]] size_type size() const ENTT_NOEXCEPT {
+        return packed.first().size();
+    }
+
+    /*! @brief Clears the container. */
+    void clear() ENTT_NOEXCEPT {
+        sparse.first().clear();
+        packed.first().clear();
+        rehash(0u);
+    }
+
+    /**
+     * @brief Inserts an element into the container, if it does not exist.
+     * @param value An element to insert into the container.
+     * @return A pair consisting of an iterator to the inserted element (or to
+     * the element that prevented the insertion) and a bool denoting whether the
+     * insertion took place.
+     */
+    std::pair<iterator, bool> insert(const value_type &value) {
+        return emplace(value);
+    }
+
+    /*! @copydoc insert */
+    std::pair<iterator, bool> insert(value_type &&value) {
+        return emplace(std::move(value));
+    }
+
+    /**
+     * @brief Inserts elements into the container, if they do not exist.
+     * @tparam It Type of input iterator.
+     * @param first An iterator to the first element of the range of elements.
+     * @param last An iterator past the last element of the range of elements.
+     */
+    template<typename It>
+    void insert(It first, It last) {
+        for(; first != last; ++first) {
+            emplace(*first);
+        }
+    }
+
+    /**
+     * @brief Constructs an element in-place, if it does not exist.
+     * @tparam Args Types of arguments to forward to the constructor of the
+     * element.
+     * @param args Arguments to forward to the constructor of the element.
+     * @return A pair consisting of an iterator to the inserted element (or to
+     * the element that prevented the insertion) and a bool denoting whether the
+     * insertion took place.
+     */
+    template<typename... Args>
+    std::pair<iterator, bool> emplace(Args &&...args) {
+        if constexpr(((sizeof...(Args) == 1u) && ... && std::is_same_v<std::remove_const_t<std::remove_reference_t<Args>>, value_type>)) {
+            return get_or_emplace(std::forward<Args>(args)...);
+        } else {
+            return get_or_emplace(value_type{std::forward<Args>(args)...});
+        }
+    }
+
+    /**
+     * @brief Removes an element from a given position.
+     * @param pos An iterator to the element to remove.
+     * @return An iterator following the removed element.
+     */
+    iterator erase(const_iterator pos) {
+        const auto dist = std::distance(cbegin(), pos);
+        erase(*pos);
+        return begin() + dist;
+    }
+
+    /**
+     * @brief Removes the given elements from a container.
+     * @param first An iterator to the first element of the range of elements.
+     * @param last An iterator past the last element of the range of elements.
+     * @return An iterator following the last removed element.
+     */
+    iterator erase(const_iterator first, const_iterator last) {
+        const auto dist = std::distance(cbegin(), first);
+
+        for(auto rfirst = std::make_reverse_iterator(last), rlast = std::make_reverse_iterator(first); rfirst != rlast; ++rfirst) {
+            erase(*rfirst);
+        }
+
+        return dist > static_cast<decltype(dist)>(size()) ? end() : (begin() + dist);
+    }
+
+    /**
+     * @brief Removes the element associated with a given value.
+     * @param value Value of an element to remove.
+     * @return Number of elements removed (either 0 or 1).
+     */
+    size_type erase(const value_type &value) {
+        return do_erase(value);
+    }
+
+    /**
+     * @brief Exchanges the contents with those of a given container.
+     * @param other Container to exchange the content with.
+     */
+    void swap(dense_hash_set &other) {
+        using std::swap;
+        swap(sparse, other.sparse);
+        swap(packed, other.packed);
+        swap(threshold, other.threshold);
+    }
+
+    /**
+     * @brief Finds an element with a given value.
+     * @param value Value of an element to search for.
+     * @return An iterator to an element with the given value. If no such
+     * element is found, a past-the-end iterator is returned.
+     */
+    [[nodiscard]] iterator find(const value_type &value) {
+        return constrained_find(value, bucket(value));
+    }
+
+    /*! @copydoc find */
+    [[nodiscard]] const_iterator find(const value_type &value) const {
+        return constrained_find(value, bucket(value));
+    }
+
+    /**
+     * @brief Finds an element that compares _equivalent_ to a given value.
+     * @tparam Other Type of an element to search for.
+     * @param value Value of an element to search for.
+     * @return An iterator to an element with the given value. If no such
+     * element is found, a past-the-end iterator is returned.
+     */
+    template<typename Other>
+    [[nodiscard]] std::enable_if_t<is_transparent_v<hasher> && is_transparent_v<key_equal>, std::conditional_t<false, Other, iterator>>
+    find(const Other &value) {
+        return constrained_find(value, bucket(value));
+    }
+
+    /*! @copydoc find */
+    template<typename Other>
+    [[nodiscard]] std::enable_if_t<is_transparent_v<hasher> && is_transparent_v<key_equal>, std::conditional_t<false, Other, const_iterator>>
+    find(const Other &value) const {
+        return constrained_find(value, bucket(value));
+    }
+
+    /**
+     * @brief Checks if the container contains an element with a given value.
+     * @param value Value of an element to search for.
+     * @return True if there is such an element, false otherwise.
+     */
+    [[nodiscard]] bool contains(const value_type &value) const {
+        return (find(value) != cend());
+    }
+
+    /**
+     * @brief Checks if the container contains an element that compares
+     * _equivalent_ to a given value.
+     * @tparam Other Type of an element to search for.
+     * @param value Value of an element to search for.
+     * @return True if there is such an element, false otherwise.
+     */
+    template<typename Other>
+    [[nodiscard]] std::enable_if_t<is_transparent_v<hasher> && is_transparent_v<key_equal>, std::conditional_t<false, Other, bool>>
+    contains(const Other &value) const {
+        return (find(value) != cend());
+    }
+
+    /**
+     * @brief Returns an iterator to the beginning of a given bucket.
+     * @param index An index of a bucket to access.
+     * @return An iterator to the beginning of the given bucket.
+     */
+    [[nodiscard]] const_local_iterator cbegin(const size_type index) const {
+        return {packed.first().data(), sparse.first()[index]};
+    }
+
+    /**
+     * @brief Returns an iterator to the beginning of a given bucket.
+     * @param index An index of a bucket to access.
+     * @return An iterator to the beginning of the given bucket.
+     */
+    [[nodiscard]] const_local_iterator begin(const size_type index) const {
+        return cbegin(index);
+    }
+
+    /**
+     * @brief Returns an iterator to the beginning of a given bucket.
+     * @param index An index of a bucket to access.
+     * @return An iterator to the beginning of the given bucket.
+     */
+    [[nodiscard]] local_iterator begin(const size_type index) {
+        return {packed.first().data(), sparse.first()[index]};
+    }
+
+    /**
+     * @brief Returns an iterator to the end of a given bucket.
+     * @param index An index of a bucket to access.
+     * @return An iterator to the end of the given bucket.
+     */
+    [[nodiscard]] const_local_iterator cend([[maybe_unused]] const size_type index) const {
+        return {packed.first().data(), std::numeric_limits<size_type>::max()};
+    }
+
+    /**
+     * @brief Returns an iterator to the end of a given bucket.
+     * @param index An index of a bucket to access.
+     * @return An iterator to the end of the given bucket.
+     */
+    [[nodiscard]] const_local_iterator end([[maybe_unused]] const size_type index) const {
+        return cend(index);
+    }
+
+    /**
+     * @brief Returns an iterator to the end of a given bucket.
+     * @param index An index of a bucket to access.
+     * @return An iterator to the end of the given bucket.
+     */
+    [[nodiscard]] local_iterator end([[maybe_unused]] const size_type index) {
+        return {packed.first().data(), std::numeric_limits<size_type>::max()};
+    }
+
+    /**
+     * @brief Returns the number of buckets.
+     * @return The number of buckets.
+     */
+    [[nodiscard]] size_type bucket_count() const {
+        return sparse.first().size();
+    }
+
+    /**
+     * @brief Returns the maximum number of buckets.
+     * @return The maximum number of buckets.
+     */
+    [[nodiscard]] size_type max_bucket_count() const {
+        return sparse.first().max_size();
+    }
+
+    /**
+     * @brief Returns the number of elements in a given bucket.
+     * @param index The index of the bucket to examine.
+     * @return The number of elements in the given bucket.
+     */
+    [[nodiscard]] size_type bucket_size(const size_type index) const {
+        return static_cast<size_type>(std::distance(begin(index), end(index)));
+    }
+
+    /**
+     * @brief Returns the bucket for a given element.
+     * @param value The value of the element to examine.
+     * @return The bucket for the given element.
+     */
+    [[nodiscard]] size_type bucket(const value_type &value) const {
+        return hash_to_bucket(sparse.second()(value));
+    }
+
+    /**
+     * @brief Returns the average number of elements per bucket.
+     * @return The average number of elements per bucket.
+     */
+    [[nodiscard]] float load_factor() const {
+        return size() / static_cast<float>(bucket_count());
+    }
+
+    /**
+     * @brief Returns the maximum average number of elements per bucket.
+     * @return The maximum average number of elements per bucket.
+     */
+    [[nodiscard]] float max_load_factor() const {
+        return threshold;
+    }
+
+    /**
+     * @brief Sets the desired maximum average number of elements per bucket.
+     * @param value A desired maximum average number of elements per bucket.
+     */
+    void max_load_factor(const float value) {
+        ENTT_ASSERT(value > 0.f, "Invalid load factor");
+        threshold = value;
+        rehash(0u);
+    }
+
+    /**
+     * @brief Reserves at least the specified number of buckets and regenerates
+     * the hash table.
+     * @param count New number of buckets.
+     */
+    void rehash(const size_type count) {
+        auto value = (std::max)(count, minimum_capacity);
+        value = (std::max)(value, static_cast<size_type>(size() / max_load_factor()));
+
+        if(const auto sz = next_power_of_two(value); sz != bucket_count()) {
+            sparse.first().resize(sz);
+            std::fill(sparse.first().begin(), sparse.first().end(), std::numeric_limits<size_type>::max());
+
+            for(size_type pos{}, last = size(); pos < last; ++pos) {
+                const auto index = bucket(packed.first()[pos].element);
+                packed.first()[pos].next = std::exchange(sparse.first()[index], pos);
+            }
+        }
+    }
+
+    /**
+     * @brief Reserves space for at least the specified number of elements and
+     * regenerates the hash table.
+     * @param count New number of elements.
+     */
+    void reserve(const size_type count) {
+        packed.first().reserve(count);
+        rehash(static_cast<size_type>(std::ceil(count / max_load_factor())));
+    }
+
+    /**
+     * @brief Returns the function used to hash the elements.
+     * @return The function used to hash the elements.
+     */
+    [[nodiscard]] hasher hash_function() const {
+        return sparse.second();
+    }
+
+    /**
+     * @brief Returns the function used to compare elements for equality.
+     * @return The function used to compare elements for equality.
+     */
+    [[nodiscard]] key_equal key_eq() const {
+        return packed.second();
+    }
+
+private:
+    compressed_pair<sparse_container_type, hasher> sparse;
+    compressed_pair<packed_container_type, key_equal> packed;
+    float threshold;
+};
+
+} // namespace entt
+
+#endif

+ 8 - 1
src/entt/container/fwd.hpp

@@ -13,6 +13,13 @@ template<
     typename = std::allocator<std::pair<const Key, Type>>>
     typename = std::allocator<std::pair<const Key, Type>>>
 class dense_hash_map;
 class dense_hash_map;
 
 
-}
+template<
+    typename Type,
+    typename = std::hash<Type>,
+    typename = std::equal_to<Type>,
+    typename = std::allocator<Type>>
+class dense_hash_set;
+
+} // namespace entt
 
 
 #endif
 #endif

+ 1 - 0
src/entt/entt.hpp

@@ -1,5 +1,6 @@
 #include "config/version.h"
 #include "config/version.h"
 #include "container/dense_hash_map.hpp"
 #include "container/dense_hash_map.hpp"
+#include "container/dense_hash_set.hpp"
 #include "core/algorithm.hpp"
 #include "core/algorithm.hpp"
 #include "core/any.hpp"
 #include "core/any.hpp"
 #include "core/attribute.h"
 #include "core/attribute.h"

+ 15 - 5
src/entt/meta/container.hpp

@@ -9,6 +9,7 @@
 #include <unordered_set>
 #include <unordered_set>
 #include <vector>
 #include <vector>
 #include "../container/dense_hash_map.hpp"
 #include "../container/dense_hash_map.hpp"
+#include "../container/dense_hash_set.hpp"
 #include "meta.hpp"
 #include "meta.hpp"
 #include "type_traits.hpp"
 #include "type_traits.hpp"
 
 
@@ -251,13 +252,22 @@ struct meta_associative_container_traits<std::unordered_set<Key, Args...>>
 
 
 /**
 /**
  * @brief Meta associative container traits for `dense_hash_map`s of any type.
  * @brief Meta associative container traits for `dense_hash_map`s of any type.
- * @tparam Key The key type of elements.
- * @tparam Value The value type of elements.
+ * @tparam Key The key type of the elements.
+ * @tparam Type The value type of the elements.
  * @tparam Args Other arguments.
  * @tparam Args Other arguments.
  */
  */
-template<typename Key, typename Value, typename... Args>
-struct meta_associative_container_traits<dense_hash_map<Key, Value, Args...>>
-    : internal::basic_meta_associative_container_traits<dense_hash_map<Key, Value, Args...>> {};
+template<typename Key, typename Type, typename... Args>
+struct meta_associative_container_traits<dense_hash_map<Key, Type, Args...>>
+    : internal::basic_meta_associative_container_traits<dense_hash_map<Key, Type, Args...>> {};
+
+/**
+ * @brief Meta associative container traits for `dense_hash_set`s of any type.
+ * @tparam Type The value type of the elements.
+ * @tparam Args Other arguments.
+ */
+template<typename Type, typename... Args>
+struct meta_associative_container_traits<dense_hash_set<Type, Args...>>
+    : internal::basic_meta_associative_container_traits<dense_hash_set<Type, Args...>> {};
 
 
 } // namespace entt
 } // namespace entt
 
 

+ 1 - 0
test/CMakeLists.txt

@@ -162,6 +162,7 @@ endif()
 # Test container
 # Test container
 
 
 SETUP_BASIC_TEST(dense_hash_map entt/container/dense_hash_map.cpp)
 SETUP_BASIC_TEST(dense_hash_map entt/container/dense_hash_map.cpp)
+SETUP_BASIC_TEST(dense_hash_set entt/container/dense_hash_set.cpp)
 
 
 # Test core
 # Test core
 
 

+ 811 - 0
test/entt/container/dense_hash_set.cpp

@@ -0,0 +1,811 @@
+#include <cmath>
+#include <functional>
+#include <iterator>
+#include <tuple>
+#include <type_traits>
+#include <utility>
+#include <gtest/gtest.h>
+#include <entt/container/dense_hash_set.hpp>
+#include <entt/core/memory.hpp>
+#include <entt/core/utility.hpp>
+
+struct transparent_equal_to {
+    using is_transparent = void;
+
+    template<typename Type, typename Other>
+    constexpr std::enable_if_t<std::is_convertible_v<Other, Type>, bool>
+    operator()(const Type &lhs, const Other &rhs) const {
+        return lhs == static_cast<Type>(rhs);
+    }
+};
+
+TEST(DenseHashSet, Functionalities) {
+    entt::dense_hash_set<std::size_t, entt::identity, transparent_equal_to> set;
+
+    ASSERT_NO_THROW([[maybe_unused]] auto alloc = set.get_allocator());
+
+    ASSERT_TRUE(set.empty());
+    ASSERT_EQ(set.size(), 0u);
+    ASSERT_EQ(set.load_factor(), 0.f);
+    ASSERT_EQ(set.max_load_factor(), .875f);
+
+    set.max_load_factor(.9f);
+
+    ASSERT_EQ(set.max_load_factor(), .9f);
+
+    ASSERT_EQ(set.begin(), set.end());
+    ASSERT_EQ(std::as_const(set).begin(), std::as_const(set).end());
+    ASSERT_EQ(set.cbegin(), set.cend());
+
+    ASSERT_NE(set.max_bucket_count(), 0u);
+    ASSERT_EQ(set.bucket_count(), 8u);
+    ASSERT_EQ(set.bucket_size(3u), 0u);
+
+    ASSERT_EQ(set.bucket(0), 0u);
+    ASSERT_EQ(set.bucket(3), 3u);
+    ASSERT_EQ(set.bucket(8), 0u);
+    ASSERT_EQ(set.bucket(10), 2u);
+
+    ASSERT_EQ(set.begin(1u), set.end(1u));
+    ASSERT_EQ(std::as_const(set).begin(1u), std::as_const(set).end(1u));
+    ASSERT_EQ(set.cbegin(1u), set.cend(1u));
+
+    ASSERT_FALSE(set.contains(42));
+    ASSERT_FALSE(set.contains(4.2));
+
+    ASSERT_EQ(set.find(42), set.end());
+    ASSERT_EQ(set.find(4.2), set.end());
+    ASSERT_EQ(std::as_const(set).find(42), set.cend());
+    ASSERT_EQ(std::as_const(set).find(4.2), set.cend());
+
+    ASSERT_EQ(set.hash_function()(42), 42);
+    ASSERT_TRUE(set.key_eq()(42, 42));
+
+    set.emplace(0u);
+
+    ASSERT_FALSE(set.empty());
+    ASSERT_EQ(set.size(), 1u);
+
+    ASSERT_NE(set.begin(), set.end());
+    ASSERT_NE(std::as_const(set).begin(), std::as_const(set).end());
+    ASSERT_NE(set.cbegin(), set.cend());
+
+    ASSERT_TRUE(set.contains(0u));
+    ASSERT_EQ(set.bucket(0u), 0u);
+
+    set.clear();
+
+    ASSERT_TRUE(set.empty());
+    ASSERT_EQ(set.size(), 0u);
+
+    ASSERT_EQ(set.begin(), set.end());
+    ASSERT_EQ(std::as_const(set).begin(), std::as_const(set).end());
+    ASSERT_EQ(set.cbegin(), set.cend());
+
+    ASSERT_FALSE(set.contains(0u));
+}
+
+TEST(DenseHashSet, Contructors) {
+    static constexpr std::size_t minimum_bucket_count = 8u;
+    entt::dense_hash_set<int> set;
+
+    ASSERT_EQ(set.bucket_count(), minimum_bucket_count);
+
+    set = entt::dense_hash_set<int>{std::allocator<int>{}};
+    set = entt::dense_hash_set<int>{2u * minimum_bucket_count, std::allocator<float>{}};
+    set = entt::dense_hash_set<int>{4u * minimum_bucket_count, std::hash<int>(), std::allocator<double>{}};
+
+    set.emplace(3);
+
+    entt::dense_hash_set<int> temp{set, set.get_allocator()};
+    entt::dense_hash_set<int> other{std::move(temp), set.get_allocator()};
+
+    ASSERT_EQ(other.size(), 1u);
+    ASSERT_EQ(other.size(), 1u);
+    ASSERT_EQ(set.bucket_count(), 4u * minimum_bucket_count);
+    ASSERT_EQ(other.bucket_count(), 4u * minimum_bucket_count);
+}
+
+TEST(DenseHashSet, Copy) {
+    entt::dense_hash_set<std::size_t, entt::identity> set;
+    set.max_load_factor(set.max_load_factor() - .05f);
+    set.emplace(3u);
+
+    entt::dense_hash_set<std::size_t, entt::identity> other{set};
+
+    ASSERT_TRUE(set.contains(3u));
+    ASSERT_TRUE(other.contains(3u));
+    ASSERT_EQ(set.max_load_factor(), other.max_load_factor());
+
+    set.emplace(1u);
+    set.emplace(11u);
+    other.emplace(0u);
+    other = set;
+
+    ASSERT_TRUE(other.contains(3u));
+    ASSERT_TRUE(other.contains(1u));
+    ASSERT_TRUE(other.contains(11u));
+    ASSERT_FALSE(other.contains(0u));
+
+    ASSERT_EQ(other.bucket(3u), set.bucket(11u));
+    ASSERT_EQ(other.bucket(3u), other.bucket(11u));
+    ASSERT_EQ(*other.begin(3u), *set.begin(3u));
+    ASSERT_EQ(*other.begin(3u), 11u);
+    ASSERT_EQ((*++other.begin(3u)), 3u);
+}
+
+TEST(DenseHashSet, Move) {
+    entt::dense_hash_set<std::size_t, entt::identity> set;
+    set.max_load_factor(set.max_load_factor() - .05f);
+    set.emplace(3u);
+
+    entt::dense_hash_set<std::size_t, entt::identity> other{std::move(set)};
+
+    ASSERT_EQ(set.size(), 0u);
+    ASSERT_TRUE(other.contains(3u));
+    ASSERT_EQ(set.max_load_factor(), other.max_load_factor());
+
+    set = other;
+    set.emplace(1u);
+    set.emplace(11u);
+    other.emplace(0u);
+    other = std::move(set);
+
+    ASSERT_EQ(set.size(), 0u);
+    ASSERT_TRUE(other.contains(3u));
+    ASSERT_TRUE(other.contains(1u));
+    ASSERT_TRUE(other.contains(11u));
+    ASSERT_FALSE(other.contains(0u));
+
+    ASSERT_EQ(other.bucket(3u), other.bucket(11u));
+    ASSERT_EQ(*other.begin(3u), 11u);
+    ASSERT_EQ(*++other.begin(3u), 3u);
+}
+
+TEST(DenseHashSet, Iterator) {
+    using iterator = typename entt::dense_hash_set<int>::iterator;
+
+    static_assert(std::is_same_v<iterator::value_type, int>);
+    static_assert(std::is_same_v<iterator::pointer, const int *>);
+    static_assert(std::is_same_v<iterator::reference, const int &>);
+
+    entt::dense_hash_set<int> set;
+    set.emplace(3);
+
+    iterator end{set.begin()};
+    iterator 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[0u], *set.begin().operator->());
+    ASSERT_EQ(begin[0u], *set.begin());
+
+    ASSERT_LT(begin, end);
+    ASSERT_LE(begin, set.begin());
+
+    ASSERT_GT(end, begin);
+    ASSERT_GE(end, set.end());
+}
+
+TEST(DenseHashSet, ConstIterator) {
+    using iterator = typename entt::dense_hash_set<int>::const_iterator;
+
+    static_assert(std::is_same_v<iterator::value_type, int>);
+    static_assert(std::is_same_v<iterator::pointer, const int *>);
+    static_assert(std::is_same_v<iterator::reference, const int &>);
+
+    entt::dense_hash_set<int> set;
+    set.emplace(3);
+
+    iterator cend{set.cbegin()};
+    iterator 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[0u], *set.cbegin().operator->());
+    ASSERT_EQ(cbegin[0u], *set.cbegin());
+
+    ASSERT_LT(cbegin, cend);
+    ASSERT_LE(cbegin, set.cbegin());
+
+    ASSERT_GT(cend, cbegin);
+    ASSERT_GE(cend, set.cend());
+}
+
+TEST(DenseHashSet, IteratorConversion) {
+    entt::dense_hash_set<int> set;
+    set.emplace(3);
+
+    typename entt::dense_hash_set<int, int>::iterator it = set.begin();
+    typename entt::dense_hash_set<int, int>::const_iterator cit = it;
+
+    static_assert(std::is_same_v<decltype(*it), const int &>);
+    static_assert(std::is_same_v<decltype(*cit), const int &>);
+
+    ASSERT_EQ(*it, 3);
+    ASSERT_EQ(*it.operator->(), 3);
+    ASSERT_EQ(it.operator->(), cit.operator->());
+    ASSERT_EQ(*it, *cit);
+
+    ASSERT_EQ(it - cit, 0);
+    ASSERT_EQ(cit - it, 0);
+    ASSERT_LE(it, cit);
+    ASSERT_LE(cit, it);
+    ASSERT_GE(it, cit);
+    ASSERT_GE(cit, it);
+    ASSERT_EQ(it, cit);
+    ASSERT_NE(++cit, it);
+}
+
+TEST(DenseHashSet, Insert) {
+    entt::dense_hash_set<int> set;
+    typename entt::dense_hash_set<int>::iterator it;
+    bool result;
+
+    ASSERT_TRUE(set.empty());
+    ASSERT_EQ(set.size(), 0u);
+    ASSERT_EQ(set.find(0), set.end());
+    ASSERT_FALSE(set.contains(0));
+
+    int value{1};
+    std::tie(it, result) = set.insert(std::as_const(value));
+
+    ASSERT_TRUE(result);
+    ASSERT_EQ(set.size(), 1u);
+    ASSERT_EQ(it, --set.end());
+    ASSERT_TRUE(set.contains(1));
+    ASSERT_NE(set.find(1), set.end());
+    ASSERT_EQ(*it, 1);
+
+    std::tie(it, result) = set.insert(value);
+
+    ASSERT_FALSE(result);
+    ASSERT_EQ(set.size(), 1u);
+    ASSERT_EQ(it, --set.end());
+    ASSERT_EQ(*it, 1);
+
+    std::tie(it, result) = set.insert(3);
+
+    ASSERT_TRUE(result);
+    ASSERT_EQ(set.size(), 2u);
+    ASSERT_EQ(it, --set.end());
+    ASSERT_TRUE(set.contains(3));
+    ASSERT_NE(set.find(3), set.end());
+    ASSERT_EQ(*it, 3);
+
+    std::tie(it, result) = set.insert(3);
+
+    ASSERT_FALSE(result);
+    ASSERT_EQ(set.size(), 2u);
+    ASSERT_EQ(it, --set.end());
+    ASSERT_EQ(*it, 3);
+
+    int range[2u]{7, 9};
+    set.insert(std::begin(range), std::end(range));
+
+    ASSERT_EQ(set.size(), 4u);
+    ASSERT_TRUE(set.contains(7));
+    ASSERT_NE(set.find(9), set.end());
+}
+
+TEST(DenseHashSet, InsertRehash) {
+    static constexpr std::size_t minimum_bucket_count = 8u;
+    entt::dense_hash_set<std::size_t, entt::identity> set;
+
+    ASSERT_EQ(set.size(), 0u);
+    ASSERT_EQ(set.bucket_count(), minimum_bucket_count);
+
+    for(std::size_t next{}; next < minimum_bucket_count; ++next) {
+        ASSERT_TRUE(set.insert(next).second);
+    }
+
+    ASSERT_EQ(set.size(), minimum_bucket_count);
+    ASSERT_GT(set.bucket_count(), minimum_bucket_count);
+    ASSERT_TRUE(set.contains(minimum_bucket_count / 2u));
+    ASSERT_EQ(set.bucket(minimum_bucket_count / 2u), minimum_bucket_count / 2u);
+    ASSERT_FALSE(set.contains(minimum_bucket_count));
+
+    ASSERT_TRUE(set.insert(minimum_bucket_count).second);
+
+    ASSERT_EQ(set.size(), minimum_bucket_count + 1u);
+    ASSERT_EQ(set.bucket_count(), minimum_bucket_count * 2u);
+    ASSERT_TRUE(set.contains(minimum_bucket_count / 2u));
+    ASSERT_EQ(set.bucket(minimum_bucket_count / 2u), minimum_bucket_count / 2u);
+    ASSERT_TRUE(set.contains(minimum_bucket_count));
+
+    for(std::size_t next{}; next <= minimum_bucket_count; ++next) {
+        ASSERT_TRUE(set.contains(next));
+        ASSERT_EQ(set.bucket(next), next);
+    }
+}
+
+TEST(DenseHashSet, InsertSameBucket) {
+    static constexpr std::size_t minimum_bucket_count = 8u;
+    entt::dense_hash_set<std::size_t, entt::identity> set;
+
+    for(std::size_t next{}; next < minimum_bucket_count; ++next) {
+        ASSERT_EQ(set.cbegin(next), set.cend(next));
+    }
+
+    ASSERT_TRUE(set.insert(1u).second);
+    ASSERT_TRUE(set.insert(9u).second);
+
+    ASSERT_EQ(set.size(), 2u);
+    ASSERT_TRUE(set.contains(1u));
+    ASSERT_NE(set.find(9u), set.end());
+    ASSERT_EQ(set.bucket(1u), 1u);
+    ASSERT_EQ(set.bucket(9u), 1u);
+    ASSERT_EQ(set.bucket_size(1u), 2u);
+    ASSERT_EQ(set.cbegin(6u), set.cend(6u));
+}
+
+TEST(DenseHashSet, Emplace) {
+    entt::dense_hash_set<int> set;
+    typename entt::dense_hash_set<int>::iterator it;
+    bool result;
+
+    ASSERT_TRUE(set.empty());
+    ASSERT_EQ(set.size(), 0u);
+    ASSERT_EQ(set.find(0), set.end());
+    ASSERT_FALSE(set.contains(0));
+
+    std::tie(it, result) = set.emplace();
+
+    ASSERT_TRUE(result);
+    ASSERT_EQ(set.size(), 1u);
+    ASSERT_EQ(it, --set.end());
+    ASSERT_TRUE(set.contains(0));
+    ASSERT_NE(set.find(0), set.end());
+    ASSERT_EQ(*it, 0);
+
+    std::tie(it, result) = set.emplace();
+
+    ASSERT_FALSE(result);
+    ASSERT_EQ(set.size(), 1u);
+    ASSERT_EQ(it, --set.end());
+    ASSERT_EQ(*it, 0);
+
+    std::tie(it, result) = set.emplace(1);
+
+    ASSERT_TRUE(result);
+    ASSERT_EQ(set.size(), 2u);
+    ASSERT_EQ(it, --set.end());
+    ASSERT_TRUE(set.contains(1));
+    ASSERT_NE(set.find(1), set.end());
+    ASSERT_EQ(*it, 1);
+
+    std::tie(it, result) = set.emplace(1);
+
+    ASSERT_FALSE(result);
+    ASSERT_EQ(set.size(), 2u);
+    ASSERT_EQ(it, --set.end());
+    ASSERT_EQ(*it, 1);
+}
+
+TEST(DenseHashSet, EmplaceRehash) {
+    static constexpr std::size_t minimum_bucket_count = 8u;
+    entt::dense_hash_set<std::size_t, entt::identity> set;
+
+    ASSERT_EQ(set.size(), 0u);
+    ASSERT_EQ(set.bucket_count(), minimum_bucket_count);
+
+    for(std::size_t next{}; next < minimum_bucket_count; ++next) {
+        ASSERT_TRUE(set.emplace(next).second);
+        ASSERT_LE(set.load_factor(), set.max_load_factor());
+    }
+
+    ASSERT_EQ(set.size(), minimum_bucket_count);
+    ASSERT_GT(set.bucket_count(), minimum_bucket_count);
+    ASSERT_TRUE(set.contains(minimum_bucket_count / 2u));
+    ASSERT_EQ(set.bucket(minimum_bucket_count / 2u), minimum_bucket_count / 2u);
+    ASSERT_FALSE(set.contains(minimum_bucket_count));
+
+    ASSERT_TRUE(set.emplace(minimum_bucket_count).second);
+
+    ASSERT_EQ(set.size(), minimum_bucket_count + 1u);
+    ASSERT_EQ(set.bucket_count(), minimum_bucket_count * 2u);
+    ASSERT_TRUE(set.contains(minimum_bucket_count / 2u));
+    ASSERT_EQ(set.bucket(minimum_bucket_count / 2u), minimum_bucket_count / 2u);
+    ASSERT_TRUE(set.contains(minimum_bucket_count));
+
+    for(std::size_t next{}; next <= minimum_bucket_count; ++next) {
+        ASSERT_TRUE(set.contains(next));
+        ASSERT_EQ(set.bucket(next), next);
+    }
+}
+
+TEST(DenseHashSet, EmplaceSameBucket) {
+    static constexpr std::size_t minimum_bucket_count = 8u;
+    entt::dense_hash_set<std::size_t, entt::identity> set;
+
+    for(std::size_t next{}; next < minimum_bucket_count; ++next) {
+        ASSERT_EQ(set.cbegin(next), set.cend(next));
+    }
+
+    ASSERT_TRUE(set.emplace(1u).second);
+    ASSERT_TRUE(set.emplace(9u).second);
+
+    ASSERT_EQ(set.size(), 2u);
+    ASSERT_TRUE(set.contains(1u));
+    ASSERT_NE(set.find(9u), set.end());
+    ASSERT_EQ(set.bucket(1u), 1u);
+    ASSERT_EQ(set.bucket(9u), 1u);
+    ASSERT_EQ(set.bucket_size(1u), 2u);
+    ASSERT_EQ(set.cbegin(6u), set.cend(6u));
+}
+
+TEST(DenseHashSet, Erase) {
+    static constexpr std::size_t minimum_bucket_count = 8u;
+    entt::dense_hash_set<std::size_t, entt::identity> set;
+
+    for(std::size_t next{}, last = minimum_bucket_count + 1u; next < last; ++next) {
+        set.emplace(next);
+    }
+
+    ASSERT_EQ(set.bucket_count(), 2 * minimum_bucket_count);
+    ASSERT_EQ(set.size(), minimum_bucket_count + 1u);
+
+    for(std::size_t next{}, last = minimum_bucket_count + 1u; next < last; ++next) {
+        ASSERT_TRUE(set.contains(next));
+    }
+
+    auto it = set.erase(++set.begin());
+    it = set.erase(it, it + 1);
+
+    ASSERT_EQ(*--set.end(), 6u);
+    ASSERT_EQ(set.erase(6u), 1u);
+    ASSERT_EQ(set.erase(6u), 0u);
+
+    ASSERT_EQ(set.bucket_count(), 2 * minimum_bucket_count);
+    ASSERT_EQ(set.size(), minimum_bucket_count + 1u - 3u);
+
+    ASSERT_EQ(it, ++set.begin());
+    ASSERT_EQ(*it, 7u);
+    ASSERT_EQ(*--set.end(), 5u);
+
+    for(std::size_t next{}, last = minimum_bucket_count + 1u; next < last; ++next) {
+        if(next == 1u || next == 8u || next == 6u) {
+            ASSERT_FALSE(set.contains(next));
+            ASSERT_EQ(set.bucket_size(next), 0u);
+        } else {
+            ASSERT_TRUE(set.contains(next));
+            ASSERT_EQ(set.bucket(next), next);
+            ASSERT_EQ(set.bucket_size(next), 1u);
+        }
+    }
+
+    set.erase(set.begin(), set.end());
+
+    for(std::size_t next{}, last = minimum_bucket_count + 1u; next < last; ++next) {
+        ASSERT_FALSE(set.contains(next));
+    }
+
+    ASSERT_EQ(set.bucket_count(), 2 * minimum_bucket_count);
+    ASSERT_EQ(set.size(), 0u);
+}
+
+TEST(DenseHashSet, EraseFromBucket) {
+    static constexpr std::size_t minimum_bucket_count = 8u;
+    entt::dense_hash_set<std::size_t, entt::identity> set;
+
+    ASSERT_EQ(set.bucket_count(), minimum_bucket_count);
+    ASSERT_EQ(set.size(), 0u);
+
+    for(std::size_t next{}; next < 4u; ++next) {
+        ASSERT_TRUE(set.emplace(2u * minimum_bucket_count * next).second);
+        ASSERT_TRUE(set.emplace(2u * minimum_bucket_count * next + 2u).second);
+        ASSERT_TRUE(set.emplace(2u * minimum_bucket_count * (next + 1u) - 1u).second);
+    }
+
+    ASSERT_EQ(set.bucket_count(), 2u * minimum_bucket_count);
+    ASSERT_EQ(set.size(), 12u);
+
+    ASSERT_EQ(set.bucket_size(0u), 4u);
+    ASSERT_EQ(set.bucket_size(2u), 4u);
+    ASSERT_EQ(set.bucket_size(15u), 4u);
+
+    set.erase(set.end() - 3, set.end());
+
+    ASSERT_EQ(set.bucket_count(), 2u * minimum_bucket_count);
+    ASSERT_EQ(set.size(), 9u);
+
+    ASSERT_EQ(set.bucket_size(0u), 3u);
+    ASSERT_EQ(set.bucket_size(2u), 3u);
+    ASSERT_EQ(set.bucket_size(15u), 3u);
+
+    for(std::size_t next{}; next < 3u; ++next) {
+        ASSERT_TRUE(set.contains(2u * minimum_bucket_count * next));
+        ASSERT_EQ(set.bucket(2u * minimum_bucket_count * next), 0u);
+
+        ASSERT_TRUE(set.contains(2u * minimum_bucket_count * next + 2u));
+        ASSERT_EQ(set.bucket(2u * minimum_bucket_count * next + 2u), 2u);
+
+        ASSERT_TRUE(set.contains(2u * minimum_bucket_count * (next + 1u) - 1u));
+        ASSERT_EQ(set.bucket(2u * minimum_bucket_count * (next + 1u) - 1u), 15u);
+    }
+
+    ASSERT_FALSE(set.contains(2u * minimum_bucket_count * 3u));
+    ASSERT_FALSE(set.contains(2u * minimum_bucket_count * 3u + 2u));
+    ASSERT_FALSE(set.contains(2u * minimum_bucket_count * (3u + 1u) - 1u));
+
+    set.erase(*++set.begin(0u));
+    set.erase(*++set.begin(2u));
+    set.erase(*++set.begin(15u));
+
+    ASSERT_EQ(set.bucket_count(), 2u * minimum_bucket_count);
+    ASSERT_EQ(set.size(), 6u);
+
+    ASSERT_EQ(set.bucket_size(0u), 2u);
+    ASSERT_EQ(set.bucket_size(2u), 2u);
+    ASSERT_EQ(set.bucket_size(15u), 2u);
+
+    ASSERT_FALSE(set.contains(2u * minimum_bucket_count * 1u));
+    ASSERT_FALSE(set.contains(2u * minimum_bucket_count * 1u + 2u));
+    ASSERT_FALSE(set.contains(2u * minimum_bucket_count * (1u + 1u) - 1u));
+
+    while(set.begin(15) != set.end(15u)) {
+        set.erase(*set.begin(15));
+    }
+
+    ASSERT_EQ(set.bucket_count(), 2u * minimum_bucket_count);
+    ASSERT_EQ(set.size(), 4u);
+
+    ASSERT_EQ(set.bucket_size(0u), 2u);
+    ASSERT_EQ(set.bucket_size(2u), 2u);
+    ASSERT_EQ(set.bucket_size(15u), 0u);
+
+    ASSERT_TRUE(set.contains(0u * minimum_bucket_count));
+    ASSERT_TRUE(set.contains(0u * minimum_bucket_count + 2u));
+    ASSERT_TRUE(set.contains(4u * minimum_bucket_count));
+    ASSERT_TRUE(set.contains(4u * minimum_bucket_count + 2u));
+
+    set.erase(4u * minimum_bucket_count + 2u);
+    set.erase(0u * minimum_bucket_count);
+
+    ASSERT_EQ(set.bucket_count(), 2u * minimum_bucket_count);
+    ASSERT_EQ(set.size(), 2u);
+
+    ASSERT_EQ(set.bucket_size(0u), 1u);
+    ASSERT_EQ(set.bucket_size(2u), 1u);
+    ASSERT_EQ(set.bucket_size(15u), 0u);
+
+    ASSERT_FALSE(set.contains(0u * minimum_bucket_count));
+    ASSERT_TRUE(set.contains(0u * minimum_bucket_count + 2u));
+    ASSERT_TRUE(set.contains(4u * minimum_bucket_count));
+    ASSERT_FALSE(set.contains(4u * minimum_bucket_count + 2u));
+}
+
+TEST(DenseHashSet, Swap) {
+    entt::dense_hash_set<int> set;
+    entt::dense_hash_set<int> other;
+
+    set.emplace(0);
+
+    ASSERT_FALSE(set.empty());
+    ASSERT_TRUE(other.empty());
+    ASSERT_TRUE(set.contains(0));
+    ASSERT_FALSE(other.contains(0));
+
+    set.swap(other);
+
+    ASSERT_TRUE(set.empty());
+    ASSERT_FALSE(other.empty());
+    ASSERT_FALSE(set.contains(0));
+    ASSERT_TRUE(other.contains(0));
+}
+
+TEST(DenseHashSet, LocalIterator) {
+    using iterator = typename entt::dense_hash_set<std::size_t, entt::identity>::local_iterator;
+
+    static_assert(std::is_same_v<iterator::value_type, std::size_t>);
+    static_assert(std::is_same_v<iterator::pointer, const std::size_t *>);
+    static_assert(std::is_same_v<iterator::reference, const std::size_t &>);
+
+    static constexpr std::size_t minimum_bucket_count = 8u;
+    entt::dense_hash_set<std::size_t, entt::identity> set;
+    set.emplace(3u);
+    set.emplace(3u + minimum_bucket_count);
+
+    iterator end{set.begin(3u)};
+    iterator begin{};
+    begin = set.end(3u);
+    std::swap(begin, end);
+
+    ASSERT_EQ(begin, set.begin(3u));
+    ASSERT_EQ(end, set.end(3u));
+    ASSERT_NE(begin, end);
+
+    ASSERT_EQ(*begin.operator->(), 3u + minimum_bucket_count);
+    ASSERT_EQ(*begin, 3u + minimum_bucket_count);
+
+    ASSERT_EQ(begin.base(), set.begin().base() + 1u);
+    ASSERT_EQ(begin++, set.begin(3u));
+    ASSERT_EQ(begin.base(), set.begin().base());
+    ASSERT_EQ(++begin, set.end(3u));
+    ASSERT_NE(begin.base(), set.end().base());
+}
+
+TEST(DenseHashSet, ConstLocalIterator) {
+    using iterator = typename entt::dense_hash_set<std::size_t, entt::identity>::const_local_iterator;
+
+    static_assert(std::is_same_v<iterator::value_type, std::size_t>);
+    static_assert(std::is_same_v<iterator::pointer, const std::size_t *>);
+    static_assert(std::is_same_v<iterator::reference, const std::size_t &>);
+
+    static constexpr std::size_t minimum_bucket_count = 8u;
+    entt::dense_hash_set<std::size_t, entt::identity> set;
+    set.emplace(3u);
+    set.emplace(3u + minimum_bucket_count);
+
+    iterator cend{set.begin(3u)};
+    iterator cbegin{};
+    cbegin = set.end(3u);
+    std::swap(cbegin, cend);
+
+    ASSERT_EQ(cbegin, set.begin(3u));
+    ASSERT_EQ(cend, set.end(3u));
+    ASSERT_NE(cbegin, cend);
+
+    ASSERT_EQ(*cbegin.operator->(), 3u + minimum_bucket_count);
+    ASSERT_EQ(*cbegin, 3u + minimum_bucket_count);
+
+    ASSERT_EQ(cbegin.base(), set.cbegin().base() + 1u);
+    ASSERT_EQ(cbegin++, set.begin(3u));
+    ASSERT_EQ(cbegin.base(), set.cbegin().base());
+    ASSERT_EQ(++cbegin, set.end(3u));
+    ASSERT_NE(cbegin.base(), set.cend().base());
+}
+
+TEST(DenseHashSet, LocalIteratorConversion) {
+    entt::dense_hash_set<int> set;
+    set.emplace(3);
+
+    typename entt::dense_hash_set<int>::local_iterator it = set.begin(set.bucket(3));
+    typename entt::dense_hash_set<int>::const_local_iterator cit = it;
+
+    static_assert(std::is_same_v<decltype(*it), const int &>);
+    static_assert(std::is_same_v<decltype(*cit), const int &>);
+
+    ASSERT_EQ(*it, 3);
+    ASSERT_EQ(*it.operator->(), 3);
+    ASSERT_EQ(it.operator->(), cit.operator->());
+    ASSERT_EQ(*it, *cit);
+
+    ASSERT_EQ(it, cit);
+    ASSERT_NE(++cit, it);
+}
+
+TEST(DenseHashSet, Rehash) {
+    static constexpr std::size_t minimum_bucket_count = 8u;
+    entt::dense_hash_set<std::size_t, entt::identity> set;
+    set.emplace(32u);
+
+    ASSERT_EQ(set.bucket_count(), minimum_bucket_count);
+    ASSERT_TRUE(set.contains(32u));
+    ASSERT_EQ(set.bucket(32u), 0u);
+
+    set.rehash(12u);
+
+    ASSERT_EQ(set.bucket_count(), 2u * minimum_bucket_count);
+    ASSERT_TRUE(set.contains(32u));
+    ASSERT_EQ(set.bucket(32u), 0u);
+
+    set.rehash(44u);
+
+    ASSERT_EQ(set.bucket_count(), 8u * minimum_bucket_count);
+    ASSERT_TRUE(set.contains(32u));
+    ASSERT_EQ(set.bucket(32u), 32u);
+
+    set.rehash(0u);
+
+    ASSERT_EQ(set.bucket_count(), minimum_bucket_count);
+    ASSERT_TRUE(set.contains(32u));
+    ASSERT_EQ(set.bucket(32u), 0u);
+
+    for(std::size_t next{}; next < minimum_bucket_count; ++next) {
+        set.emplace(next);
+    }
+
+    ASSERT_EQ(set.size(), minimum_bucket_count + 1u);
+    ASSERT_EQ(set.bucket_count(), 2u * minimum_bucket_count);
+
+    set.rehash(0u);
+
+    ASSERT_EQ(set.bucket_count(), 2u * minimum_bucket_count);
+    ASSERT_TRUE(set.contains(32u));
+
+    set.rehash(55u);
+
+    ASSERT_EQ(set.bucket_count(), 8u * minimum_bucket_count);
+    ASSERT_TRUE(set.contains(32u));
+
+    set.rehash(2u);
+
+    ASSERT_EQ(set.bucket_count(), 2u * minimum_bucket_count);
+    ASSERT_TRUE(set.contains(32u));
+    ASSERT_EQ(set.bucket(32u), 0u);
+
+    for(std::size_t next{}; next < minimum_bucket_count; ++next) {
+        ASSERT_TRUE(set.contains(next));
+        ASSERT_EQ(set.bucket(next), next);
+    }
+
+    ASSERT_EQ(set.bucket_size(0u), 2u);
+    ASSERT_EQ(set.bucket_size(3u), 1u);
+
+    ASSERT_EQ(*set.begin(0u), 0u);
+    ASSERT_EQ(*++set.begin(0u), 32u);
+
+    set.clear();
+    set.rehash(2u);
+
+    ASSERT_EQ(set.bucket_count(), minimum_bucket_count);
+    ASSERT_FALSE(set.contains(32u));
+
+    for(std::size_t next{}; next < minimum_bucket_count; ++next) {
+        ASSERT_FALSE(set.contains(next));
+    }
+
+    ASSERT_EQ(set.bucket_size(0u), 0u);
+    ASSERT_EQ(set.bucket_size(3u), 0u);
+}
+
+TEST(DenseHashSet, Reserve) {
+    static constexpr std::size_t minimum_bucket_count = 8u;
+    entt::dense_hash_set<int> set;
+
+    ASSERT_EQ(set.bucket_count(), minimum_bucket_count);
+
+    set.reserve(0u);
+
+    ASSERT_EQ(set.bucket_count(), minimum_bucket_count);
+
+    set.reserve(minimum_bucket_count);
+
+    ASSERT_EQ(set.bucket_count(), 2 * minimum_bucket_count);
+    ASSERT_EQ(set.bucket_count(), entt::next_power_of_two(std::ceil(minimum_bucket_count / set.max_load_factor())));
+}

+ 46 - 0
test/entt/meta/meta_container.cpp

@@ -322,6 +322,52 @@ TEST_F(MetaContainer, DenseHashMap) {
     ASSERT_EQ(view.size(), 0u);
     ASSERT_EQ(view.size(), 0u);
 }
 }
 
 
+TEST_F(MetaContainer, DenseHashSet) {
+    entt::dense_hash_set<int> set{};
+    auto any = entt::forward_as_meta(set);
+    auto view = any.as_associative_container();
+
+    set.emplace(2);
+    set.emplace(3);
+    set.emplace(4);
+
+    ASSERT_TRUE(view);
+    ASSERT_TRUE(view.key_only());
+    ASSERT_EQ(view.key_type(), entt::resolve<int>());
+    ASSERT_EQ(view.mapped_type(), entt::meta_type{});
+    ASSERT_EQ(view.value_type(), entt::resolve<int>());
+
+    ASSERT_EQ(view.size(), 3u);
+    ASSERT_NE(view.begin(), view.end());
+
+    ASSERT_EQ(view.find(3)->first.cast<int>(), 3);
+
+    ASSERT_FALSE(view.insert(invalid_type{}));
+
+    ASSERT_TRUE(view.insert(.0));
+    ASSERT_TRUE(view.insert(1));
+
+    ASSERT_EQ(view.size(), 5u);
+    ASSERT_EQ(view.find(0)->first.cast<int>(), 0);
+    ASSERT_EQ(view.find(1.)->first.cast<int>(), 1);
+
+    ASSERT_FALSE(view.erase(invalid_type{}));
+    ASSERT_FALSE(view.find(invalid_type{}));
+    ASSERT_EQ(view.size(), 5u);
+
+    ASSERT_TRUE(view.erase(0));
+    ASSERT_EQ(view.size(), 4u);
+    ASSERT_EQ(view.find(0), view.end());
+
+    ASSERT_EQ(view.find(1.f)->first.try_cast<int>(), nullptr);
+    ASSERT_NE(view.find(1.)->first.try_cast<const int>(), nullptr);
+    ASSERT_EQ(view.find(true)->first.cast<const int &>(), 1);
+
+    ASSERT_TRUE(view.erase(1.));
+    ASSERT_TRUE(view.clear());
+    ASSERT_EQ(view.size(), 0u);
+}
+
 TEST_F(MetaContainer, ConstSequenceContainer) {
 TEST_F(MetaContainer, ConstSequenceContainer) {
     std::vector<int> vec{};
     std::vector<int> vec{};
     auto any = entt::forward_as_meta(std::as_const(vec));
     auto any = entt::forward_as_meta(std::as_const(vec));