Parcourir la source

container: added dense_hash_set

Michele Caini il y a 4 ans
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/version.h>
             $<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/core/algorithm.hpp>
             $<BUILD_INTERFACE:${EnTT_SOURCE_DIR}/src/entt/core/any.hpp>

+ 13 - 0
docs/md/container.md

@@ -8,6 +8,7 @@
 * [Introduction](#introduction)
 * [Containers](#containers)
   * [Dense hash map](#dense-hash-map)
+  * [Dense hash set](#dense-hash-set)
 
 <!--
 @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
 library, that is, `std::unordered_map`.<br/>
 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>>>
 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

+ 1 - 0
src/entt/entt.hpp

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

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

@@ -9,6 +9,7 @@
 #include <unordered_set>
 #include <vector>
 #include "../container/dense_hash_map.hpp"
+#include "../container/dense_hash_set.hpp"
 #include "meta.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.
- * @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.
  */
-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
 

+ 1 - 0
test/CMakeLists.txt

@@ -162,6 +162,7 @@ endif()
 # Test container
 
 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
 

+ 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);
 }
 
+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) {
     std::vector<int> vec{};
     auto any = entt::forward_as_meta(std::as_const(vec));