Răsfoiți Sursa

container: sparse set based dense hash map

Michele Caini 4 ani în urmă
părinte
comite
af249098cd

+ 2 - 2
README.md

@@ -73,8 +73,7 @@ This project started off as a pure entity-component system. Over time the
 codebase has grown as more and more classes and functionalities were added.<br/>
 Here is a brief, yet incomplete list of what it offers today:
 
-* Statically generated integer **identifiers** for types (assigned either at
-  compile-time or at runtime).
+* A built-in **RTTI system** mostly similar to the standard one.
 * A `constexpr` utility for human readable **resource names**.
 * A minimal **configuration system** built using the monostate pattern.
 * An incredibly fast **entity-component system** based on sparse sets, with its
@@ -88,6 +87,7 @@ Here is a brief, yet incomplete list of what it offers today:
 * The smallest and most basic implementation of a **service locator** ever seen.
 * A built-in, non-intrusive and macro-free runtime **reflection system**.
 * **Static polymorphism** made simple and within everyone's reach.
+* A few homemade containers, like a sparse set based **dense hash map**.
 * A **cooperative scheduler** for processes of any type.
 * All that is needed for **resource management** (cache, loaders, handles).
 * Delegates, **signal handlers** (with built-in support for collectors) and a

+ 1 - 0
docs/CMakeLists.txt

@@ -17,6 +17,7 @@ add_custom_target(
     SOURCES
         dox/extra.dox
         md/config.md
+        md/container.md
         md/core.md
         md/entity.md
         md/faq.md

+ 43 - 0
docs/md/container.md

@@ -0,0 +1,43 @@
+# Crash Course: containers
+
+<!--
+@cond TURN_OFF_DOXYGEN
+-->
+# Table of Contents
+
+* [Introduction](#introduction)
+* [Containers](#containers)
+  * [Dense hash map](#dense-hash-map)
+
+<!--
+@endcond TURN_OFF_DOXYGEN
+-->
+
+# Introduction
+
+The standard C++ library offers a wide range of containers and it's really
+difficult to do better (although it's very easy to do worse, as many examples
+available online demonstrate).<br/>
+`EnTT` doesn't try in any way to replace what is offered by the standard. Quite
+the opposite, given the widespread use that is made of standard containers.<br/>
+However, the library also tries to fill a gap in features and functionality by
+making available some containers initially developed for internal use.
+
+This section of the library is likely to grow larger over time. However, for the
+moment it's quite small and mainly aimed at satisfying some internal needs.<br/>
+For all containers made available, full test coverage and stability over time is
+guaranteed as usual.
+
+# Containers
+
+## Dense hash map
+
+The dense hash map made available in `EnTT` is a map 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_map`.<br/>
+Therefore, there is no need to go into the API description.

+ 940 - 0
src/entt/container/dense_hash_map.hpp

@@ -0,0 +1,940 @@
+#ifndef ENTT_CONTAINER_DENSE_HASH_MAP_HPP
+#define ENTT_CONTAINER_DENSE_HASH_MAP_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 Key, typename Type>
+struct dense_hash_map_node final {
+    template<typename... Args>
+    dense_hash_map_node(const std::size_t pos, Args &&...args)
+        : next{pos},
+          element{std::forward<Args>(args)...} {}
+
+    std::size_t next;
+    std::pair<Key, Type> element;
+};
+
+template<typename It>
+class dense_hash_map_iterator {
+    friend dense_hash_map_iterator<const std::remove_pointer_t<It> *>;
+
+    using iterator_traits = std::iterator_traits<decltype(std::addressof(std::declval<It>()->element))>;
+
+public:
+    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_map_iterator() ENTT_NOEXCEPT = default;
+
+    dense_hash_map_iterator(const It iter) ENTT_NOEXCEPT
+        : it{iter} {}
+
+    template<bool Const = std::is_const_v<std::remove_pointer_t<It>>, typename = std::enable_if_t<Const>>
+    dense_hash_map_iterator(const dense_hash_map_iterator<std::remove_const_t<std::remove_pointer_t<It>> *> &other)
+        : it{other.it} {}
+
+    dense_hash_map_iterator &operator++() ENTT_NOEXCEPT {
+        return ++it, *this;
+    }
+
+    dense_hash_map_iterator operator++(int) ENTT_NOEXCEPT {
+        dense_hash_map_iterator orig = *this;
+        return ++(*this), orig;
+    }
+
+    dense_hash_map_iterator &operator--() ENTT_NOEXCEPT {
+        return --it, *this;
+    }
+
+    dense_hash_map_iterator operator--(int) ENTT_NOEXCEPT {
+        dense_hash_map_iterator orig = *this;
+        return operator--(), orig;
+    }
+
+    dense_hash_map_iterator &operator+=(const difference_type value) ENTT_NOEXCEPT {
+        it += value;
+        return *this;
+    }
+
+    dense_hash_map_iterator operator+(const difference_type value) const ENTT_NOEXCEPT {
+        dense_hash_map_iterator copy = *this;
+        return (copy += value);
+    }
+
+    dense_hash_map_iterator &operator-=(const difference_type value) ENTT_NOEXCEPT {
+        return (*this += -value);
+    }
+
+    dense_hash_map_iterator operator-(const difference_type value) const ENTT_NOEXCEPT {
+        return (*this + -value);
+    }
+
+    difference_type operator-(const dense_hash_map_iterator &other) const ENTT_NOEXCEPT {
+        return it - other.it;
+    }
+
+    [[nodiscard]] reference operator[](const difference_type value) const {
+        return it->element;
+    }
+
+    [[nodiscard]] bool operator==(const dense_hash_map_iterator &other) const ENTT_NOEXCEPT {
+        return it == other.it;
+    }
+
+    [[nodiscard]] bool operator!=(const dense_hash_map_iterator &other) const ENTT_NOEXCEPT {
+        return !(*this == other);
+    }
+
+    [[nodiscard]] bool operator<(const dense_hash_map_iterator &other) const ENTT_NOEXCEPT {
+        return it < other.it;
+    }
+
+    [[nodiscard]] bool operator>(const dense_hash_map_iterator &other) const ENTT_NOEXCEPT {
+        return it > other.it;
+    }
+
+    [[nodiscard]] bool operator<=(const dense_hash_map_iterator &other) const ENTT_NOEXCEPT {
+        return !(*this > other);
+    }
+
+    [[nodiscard]] bool operator>=(const dense_hash_map_iterator &other) const ENTT_NOEXCEPT {
+        return !(*this < other);
+    }
+
+    [[nodiscard]] pointer operator->() const {
+        return std::addressof(it->element);
+    }
+
+    [[nodiscard]] reference operator*() const {
+        return *operator->();
+    }
+
+private:
+    It it;
+};
+
+template<typename It>
+class dense_hash_map_local_iterator {
+    friend dense_hash_map_local_iterator<const std::remove_pointer_t<It> *>;
+
+    using iterator_traits = std::iterator_traits<decltype(std::addressof(std::declval<It>()->element))>;
+
+public:
+    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_map_local_iterator() ENTT_NOEXCEPT = default;
+
+    dense_hash_map_local_iterator(It iter, const std::size_t pos) ENTT_NOEXCEPT
+        : it{iter},
+          curr{pos} {}
+
+    template<bool Const = std::is_const_v<std::remove_pointer_t<It>>, typename = std::enable_if_t<Const>>
+    dense_hash_map_local_iterator(const dense_hash_map_local_iterator<std::remove_const_t<std::remove_pointer_t<It>> *> &other)
+        : it{other.it},
+          curr{other.curr} {}
+
+    dense_hash_map_local_iterator &operator++() ENTT_NOEXCEPT {
+        return curr = it[curr].next, *this;
+    }
+
+    dense_hash_map_local_iterator operator++(int) ENTT_NOEXCEPT {
+        dense_hash_map_local_iterator orig = *this;
+        return ++(*this), orig;
+    }
+
+    [[nodiscard]] bool operator==(const dense_hash_map_local_iterator &other) const ENTT_NOEXCEPT {
+        return curr == other.curr;
+    }
+
+    [[nodiscard]] bool operator!=(const dense_hash_map_local_iterator &other) const ENTT_NOEXCEPT {
+        return !(*this == other);
+    }
+
+    [[nodiscard]] pointer operator->() const {
+        return std::addressof(it[curr].element);
+    }
+
+    [[nodiscard]] reference operator*() const {
+        return *operator->();
+    }
+
+    [[nodiscard]] auto index() const ENTT_NOEXCEPT {
+        return curr;
+    }
+
+private:
+    It it;
+    std::size_t curr;
+};
+
+} // namespace internal
+
+/**
+ * Internal details not to be documented.
+ * @endcond
+ */
+
+/**
+ * @brief Associative container for key-value pairs with unique keys.
+ *
+ * Internally, elements are organized into buckets. Which bucket an element is
+ * placed into depends entirely on the hash of its key. Keys with the same hash
+ * code appear in the same bucket.
+ *
+ * @tparam Key Key type of the associative container.
+ * @tparam Type Mapped type of the associative container.
+ * @tparam Hash Type of function to use to hash the keys.
+ * @tparam KeyEqual Type of function to use to compare the keys for equality.
+ * @tparam Allocator Type of allocator used to manage memory and elements.
+ */
+template<typename Key, typename Type, typename Hash, typename KeyEqual, typename Allocator>
+class dense_hash_map 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<std::pair<const Key, Type>>;
+    using alloc_traits = typename std::allocator_traits<alloc>;
+
+    using node_type = internal::dense_hash_map_node<const Key, 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 &key, std::size_t bucket) {
+        for(auto it = begin(bucket), last = end(bucket); it != last; ++it) {
+            if(packed.second()(it->first, key)) {
+                return iterator{packed.first().data() + it.index()};
+            }
+        }
+
+        return end();
+    }
+
+    template<typename Other>
+    [[nodiscard]] auto constrained_find(const Other &key, std::size_t bucket) const {
+        for(auto it = begin(bucket), last = end(bucket); it != last; ++it) {
+            if(packed.second()(it->first, key)) {
+                return const_iterator{packed.first().data() + it.index()};
+            }
+        }
+
+        return cend();
+    }
+
+    template<typename... Args>
+    [[nodiscard]] auto get_or_emplace(const Key &key, Args &&...args) {
+        const auto hash = sparse.second()(key);
+        auto index = hash_to_bucket(hash);
+
+        if(auto it = constrained_find(key, index); it != end()) {
+            return std::make_pair(it, false);
+        }
+
+        if(const auto count = size() + 1u; count > (bucket_count() * max_load_factor())) {
+            rehash(count);
+            index = hash_to_bucket(hash);
+        }
+
+        packed.first().emplace_back(sparse.first()[index], std::forward<Args>(args)...);
+        // update goes after emplace to enforce exception guarantees
+        sparse.first()[index] = size() - 1u;
+
+        return std::make_pair(--end(), true);
+    }
+
+    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.first);
+            for(; *curr != last; curr = &packed.first()[*curr].next) {}
+            *curr = pos;
+
+            auto allocator = packed.first().get_allocator();
+            auto *ptr = packed.first().data() + pos;
+
+            std::destroy_at(ptr);
+            // no exception guarantees when mapped type has a throwing move constructor
+            using node_allocator_traits = typename alloc_traits::template rebind_traits<node_type>;
+            node_allocator_traits::construct(allocator, ptr, std::move(packed.first()[last]));
+        }
+
+        packed.first().pop_back();
+    }
+
+public:
+    /*! @brief Key type of the container. */
+    using key_type = Key;
+    /*! @brief Mapped type of the container. */
+    using mapped_type = Type;
+    /*! @brief Key-value type of the container. */
+    using value_type = std::pair<const Key, Type>;
+    /*! @brief Unsigned integer type. */
+    using size_type = std::size_t;
+    /*! @brief Type of function to use to hash the keys. */
+    using hasher = Hash;
+    /*! @brief Type of function to use to compare the keys for equality. */
+    using key_equal = KeyEqual;
+    /*! @brief Allocator type. */
+    using allocator_type = Allocator;
+    /*! @brief Random access iterator type. */
+    using iterator = internal::dense_hash_map_iterator<typename packed_container_type::pointer>;
+    /*! @brief Constant random access iterator type. */
+    using const_iterator = internal::dense_hash_map_iterator<typename packed_container_type::const_pointer>;
+    /*! @brief Forward iterator type. */
+    using local_iterator = internal::dense_hash_map_local_iterator<typename packed_container_type::pointer>;
+    /*! @brief Constant forward iterator type. */
+    using const_local_iterator = internal::dense_hash_map_local_iterator<typename packed_container_type::const_pointer>;
+
+    /*! @brief Default constructor. */
+    dense_hash_map()
+        : dense_hash_map(minimum_capacity) {}
+
+    /**
+     * @brief Constructs an empty container with a given allocator.
+     * @param allocator The allocator to use.
+     */
+    explicit dense_hash_map(const allocator_type &allocator)
+        : dense_hash_map{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_map(const size_type bucket_count, const allocator_type &allocator)
+        : dense_hash_map{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_map(const size_type bucket_count, const hasher &hash, const allocator_type &allocator)
+        : dense_hash_map{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_map(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_map(const dense_hash_map &other)
+        : dense_hash_map{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_map(const dense_hash_map &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_map(dense_hash_map &&other) ENTT_NOEXCEPT = default;
+
+    /**
+     * @brief Allocator-extended move constructor.
+     * @param other The instance to move from.
+     * @param allocator The allocator to use.
+     */
+    dense_hash_map(dense_hash_map &&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_map() = default;
+
+    /**
+     * @brief Copy assignment operator.
+     * @param other The instance to copy from.
+     * @return This container.
+     */
+    dense_hash_map &operator=(const dense_hash_map &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_map &operator=(dense_hash_map &&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 the key does not exist.
+     * @param value A key-value pair eventually convertible to the value type.
+     * @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));
+    }
+
+    /**
+     * @copydoc insert
+     * @tparam Arg Type of the key-value pair to insert into the container.
+     */
+    template<typename Arg>
+    std::enable_if_t<std::is_constructible_v<value_type, Arg &&>, std::pair<iterator, bool>>
+    insert(Arg &&value) {
+        return emplace(std::forward<Arg>(value));
+    }
+
+    /**
+     * @brief Inserts elements into the container, if their keys 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 Inserts an element into the container or assigns to the current
+     * element if the key already exists.
+     * @tparam Arg Type of the value to insert or assign.
+     * @param key A key used both to look up and to insert if not found.
+     * @param value A value to insert or assign.
+     * @return A pair consisting of an iterator to the element and a bool
+     * denoting whether the insertion took place.
+     */
+    template<typename Arg>
+    std::pair<iterator, bool> insert_or_assign(const key_type &key, Arg &&value) {
+        auto result = try_emplace(key, std::forward<Arg>(value));
+
+        if(!result.second) {
+            result.first->second = std::forward<Arg>(value);
+        }
+
+        return result;
+    }
+
+    /*! @copydoc insert_or_assign */
+    template<typename Arg>
+    std::pair<iterator, bool> insert_or_assign(key_type &&key, Arg &&value) {
+        auto result = try_emplace(std::move(key), std::forward<Arg>(value));
+
+        if(!result.second) {
+            result.first->second = std::forward<Arg>(value);
+        }
+
+        return result;
+    }
+
+    /**
+     * @brief Constructs an element in-place, if the key 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) == 0u) {
+            return get_or_emplace(key_type{});
+        } else if constexpr(sizeof...(Args) == 1u) {
+            return get_or_emplace(args.first..., std::forward<Args>(args)...);
+        } else if constexpr(sizeof...(Args) == 2u) {
+            return get_or_emplace(std::get<0u>(std::tie(args...)), std::forward<Args>(args)...);
+        } else {
+            static_assert(sizeof...(Args) == 3u, "Invalid arguments");
+            return emplace(std::pair<key_type, mapped_type>{std::forward<Args>(args)...});
+        }
+    }
+
+    /**
+     * @brief Inserts in-place if the key does not exist, does nothing if the
+     * key exists.
+     * @tparam Args Types of arguments to forward to the constructor of the
+     * element.
+     * @param key A key used both to look up and to insert if not found.
+     * @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> try_emplace(const key_type &key, Args &&...args) {
+        return get_or_emplace(key, std::piecewise_construct, std::forward_as_tuple(key), std::forward_as_tuple(std::forward<Args>(args)...));
+    }
+
+    /*! @copydoc try_emplace */
+    template<typename... Args>
+    std::pair<iterator, bool> try_emplace(key_type &&key, Args &&...args) {
+        return get_or_emplace(key, std::piecewise_construct, std::forward_as_tuple(std::move(key)), std::forward_as_tuple(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->first);
+        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->first);
+        }
+
+        return dist > static_cast<decltype(dist)>(size()) ? end() : (begin() + dist);
+    }
+
+    /**
+     * @brief Removes the element associated with a given key.
+     * @param key A key value of an element to remove.
+     * @return Number of elements removed (either 0 or 1).
+     */
+    size_type erase(const key_type &key) {
+        for(size_type *curr = sparse.first().data() + bucket(key); *curr != std::numeric_limits<size_type>::max(); curr = &packed.first()[*curr].next) {
+            if(packed.second()(packed.first()[*curr].element.first, key)) {
+                const auto index = *curr;
+                *curr = packed.first()[*curr].next;
+                move_and_pop(index);
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * @brief Exchanges the contents with those of a given container.
+     * @param other Container to exchange the content with.
+     */
+    void swap(dense_hash_map &other) {
+        using std::swap;
+        swap(sparse, other.sparse);
+        swap(packed, other.packed);
+        swap(threshold, other.threshold);
+    }
+
+    /**
+     * @brief Accesses a given element with bounds checking.
+     * @param key A key of an element to find.
+     * @return A reference to the mapped value of the requested element.
+     */
+    [[nodiscard]] Type &at(const key_type &key) {
+        auto it = find(key);
+        ENTT_ASSERT(it != end(), "Invalid key");
+        return it->second;
+    }
+
+    /*! @copydoc at */
+    [[nodiscard]] const Type &at(const key_type &key) const {
+        auto it = find(key);
+        ENTT_ASSERT(it != cend(), "Invalid key");
+        return it->second;
+    }
+
+    /**
+     * @brief Accesses or inserts a given element.
+     * @param key A key of an element to find or insert.
+     * @return A reference to the mapped value of the requested element.
+     */
+    [[nodiscard]] Type &operator[](const key_type &key) {
+        return try_emplace(key).first->second;
+    }
+
+    /**
+     * @brief Accesses or inserts a given element.
+     * @param key A key of an element to find or insert.
+     * @return A reference to the mapped value of the requested element.
+     */
+    [[nodiscard]] Type &operator[](key_type &&key) {
+        return try_emplace(std::move(key)).first->second;
+    }
+
+    /**
+     * @brief Finds an element with a given key.
+     * @param key Key value of an element to search for.
+     * @return An iterator to an element with the given key. If no such element
+     * is found, a past-the-end iterator is returned.
+     */
+    [[nodiscard]] iterator find(const key_type &key) {
+        return constrained_find(key, bucket(key));
+    }
+
+    /*! @copydoc find */
+    [[nodiscard]] const_iterator find(const key_type &key) const {
+        return constrained_find(key, bucket(key));
+    }
+
+    /**
+     * @brief Finds an element with a key that compares _equivalent_ to a given
+     * value.
+     * @tparam Other Type of the key value of the element to search for.
+     * @param key Key value of an element to search for.
+     * @return An iterator to an element with the given key. 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 &key) {
+        return constrained_find(key, bucket(key));
+    }
+
+    /*! @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 &key) const {
+        return constrained_find(key, bucket(key));
+    }
+
+    /**
+     * @brief Checks if the container contains an element with a given key.
+     * @param key Key value of an element to search for.
+     * @return True if there is such an element, false otherwise.
+     */
+    [[nodiscard]] bool contains(const key_type &key) const {
+        return (find(key) != cend());
+    }
+
+    /**
+     * @brief Checks if the container contains an element with a key that
+     * compares _equivalent_ to a given value.
+     * @tparam Other Type of the key value of the element to search for.
+     * @param key Key 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 &key) const {
+        return (find(key) != 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 {nullptr, std::numeric_limits<std::size_t>::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 {nullptr, std::numeric_limits<std::size_t>::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 key.
+     * @param key The value of the key to examine.
+     * @return The bucket for the given key.
+     */
+    [[nodiscard]] size_type bucket(const key_type &key) const {
+        return hash_to_bucket(sparse.second()(key));
+    }
+
+    /**
+     * @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.first);
+                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 keys.
+     * @return The function used to hash the keys.
+     */
+    [[nodiscard]] hasher hash_function() const {
+        return sparse.second();
+    }
+
+    /**
+     * @brief Returns the function used to compare keys for equality.
+     * @return The function used to compare keys 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

+ 18 - 0
src/entt/container/fwd.hpp

@@ -0,0 +1,18 @@
+#ifndef ENTT_CONTAINER_FWD_HPP
+#define ENTT_CONTAINER_FWD_HPP
+
+#include <functional>
+#include <memory>
+
+namespace entt {
+
+template<
+    typename Key, typename Type,
+    typename = std::hash<Key>,
+    typename = std::equal_to<Key>,
+    typename = std::allocator<std::pair<const Key, Type>>>
+class dense_hash_map;
+
+}
+
+#endif

+ 2 - 2
src/entt/core/compressed_pair.hpp

@@ -23,7 +23,7 @@ struct compressed_pair_element {
     compressed_pair_element()
         : value{} {}
 
-    template<typename Args, typename = std::enable_if_t<!std::is_same_v<std::decay_t<Args>, compressed_pair_element>>>
+    template<typename Args, typename = std::enable_if_t<!std::is_same_v<std::remove_const_t<std::remove_reference_t<Args>>, compressed_pair_element>>>
     compressed_pair_element(Args &&args)
         : value{std::forward<Args>(args)} {}
 
@@ -49,7 +49,7 @@ struct compressed_pair_element<Type, Tag, std::enable_if_t<is_ebco_eligible_v<Ty
     compressed_pair_element()
         : Type{} {}
 
-    template<typename Args, typename = std::enable_if_t<!std::is_same_v<std::decay_t<Args>, compressed_pair_element>>>
+    template<typename Args, typename = std::enable_if_t<!std::is_same_v<std::remove_const_t<std::remove_reference_t<Args>>, compressed_pair_element>>>
     compressed_pair_element(Args &&args)
         : Type{std::forward<Args>(args)} {}
 

+ 1 - 0
src/entt/entt.hpp

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

+ 1 - 0
src/entt/fwd.hpp

@@ -1,3 +1,4 @@
+#include "container/fwd.hpp"
 #include "core/fwd.hpp"
 #include "entity/fwd.hpp"
 #include "meta/fwd.hpp"

+ 4 - 0
test/CMakeLists.txt

@@ -159,6 +159,10 @@ if(ENTT_BUILD_SNAPSHOT)
     target_include_directories(cereal PRIVATE ${cereal_INCLUDE_DIR})
 endif()
 
+# Test container
+
+SETUP_BASIC_TEST(dense_hash_map entt/container/dense_hash_map.cpp)
+
 # Test core
 
 SETUP_BASIC_TEST(algorithm entt/core/algorithm.cpp)

+ 1053 - 0
test/entt/container/dense_hash_map.cpp

@@ -0,0 +1,1053 @@
+#include <cmath>
+#include <functional>
+#include <iterator>
+#include <tuple>
+#include <type_traits>
+#include <utility>
+#include <gtest/gtest.h>
+#include <entt/container/dense_hash_map.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(DenseHashMap, Functionalities) {
+    entt::dense_hash_map<std::size_t, std::size_t, entt::identity, transparent_equal_to> map;
+
+    ASSERT_NO_THROW([[maybe_unused]] auto alloc = map.get_allocator());
+
+    ASSERT_TRUE(map.empty());
+    ASSERT_EQ(map.size(), 0u);
+    ASSERT_EQ(map.load_factor(), 0.f);
+    ASSERT_EQ(map.max_load_factor(), .875f);
+
+    map.max_load_factor(.9f);
+
+    ASSERT_EQ(map.max_load_factor(), .9f);
+
+    ASSERT_EQ(map.begin(), map.end());
+    ASSERT_EQ(std::as_const(map).begin(), std::as_const(map).end());
+    ASSERT_EQ(map.cbegin(), map.cend());
+
+    ASSERT_NE(map.max_bucket_count(), 0u);
+    ASSERT_EQ(map.bucket_count(), 8u);
+    ASSERT_EQ(map.bucket_size(3u), 0u);
+
+    ASSERT_EQ(map.bucket(0), 0u);
+    ASSERT_EQ(map.bucket(3), 3u);
+    ASSERT_EQ(map.bucket(8), 0u);
+    ASSERT_EQ(map.bucket(10), 2u);
+
+    ASSERT_EQ(map.begin(1u), map.end(1u));
+    ASSERT_EQ(std::as_const(map).begin(1u), std::as_const(map).end(1u));
+    ASSERT_EQ(map.cbegin(1u), map.cend(1u));
+
+    ASSERT_FALSE(map.contains(42));
+    ASSERT_FALSE(map.contains(4.2));
+
+    ASSERT_EQ(map.find(42), map.end());
+    ASSERT_EQ(map.find(4.2), map.end());
+    ASSERT_EQ(std::as_const(map).find(42), map.cend());
+    ASSERT_EQ(std::as_const(map).find(4.2), map.cend());
+
+    ASSERT_EQ(map.hash_function()(42), 42);
+    ASSERT_TRUE(map.key_eq()(42, 42));
+
+    map.emplace(0u, 0u);
+
+    ASSERT_FALSE(map.empty());
+    ASSERT_EQ(map.size(), 1u);
+
+    ASSERT_NE(map.begin(), map.end());
+    ASSERT_NE(std::as_const(map).begin(), std::as_const(map).end());
+    ASSERT_NE(map.cbegin(), map.cend());
+
+    ASSERT_TRUE(map.contains(0u));
+    ASSERT_EQ(map.bucket(0u), 0u);
+
+    map.clear();
+
+    ASSERT_TRUE(map.empty());
+    ASSERT_EQ(map.size(), 0u);
+
+    ASSERT_EQ(map.begin(), map.end());
+    ASSERT_EQ(std::as_const(map).begin(), std::as_const(map).end());
+    ASSERT_EQ(map.cbegin(), map.cend());
+
+    ASSERT_FALSE(map.contains(0u));
+}
+
+TEST(DenseHashMap, Contructors) {
+    static constexpr std::size_t expected_bucket_count = 8u;
+    entt::dense_hash_map<int, int> map;
+
+    ASSERT_EQ(map.bucket_count(), expected_bucket_count);
+
+    map = entt::dense_hash_map<int, int>{std::allocator<int>{}};
+    map = entt::dense_hash_map<int, int>{2u * expected_bucket_count, std::allocator<float>{}};
+    map = entt::dense_hash_map<int, int>{4u * expected_bucket_count, std::hash<int>(), std::allocator<double>{}};
+
+    map.emplace(3u, 42u);
+
+    entt::dense_hash_map<int, int> temp{map, map.get_allocator()};
+    entt::dense_hash_map<int, int> other{std::move(temp), map.get_allocator()};
+
+    ASSERT_EQ(other.size(), 1u);
+    ASSERT_EQ(other.size(), 1u);
+    ASSERT_EQ(map.bucket_count(), 4u * expected_bucket_count);
+    ASSERT_EQ(other.bucket_count(), 4u * expected_bucket_count);
+}
+
+TEST(DenseHashMap, Copy) {
+    entt::dense_hash_map<std::size_t, std::size_t, entt::identity> map;
+    map.max_load_factor(map.max_load_factor() - .05f);
+    map.emplace(3u, 42u);
+
+    entt::dense_hash_map<std::size_t, std::size_t, entt::identity> other{map};
+
+    ASSERT_TRUE(map.contains(3u));
+    ASSERT_TRUE(other.contains(3u));
+    ASSERT_EQ(map.max_load_factor(), other.max_load_factor());
+
+    map.emplace(1u, 99u);
+    map.emplace(11u, 77u);
+    other.emplace(0u, 0u);
+    other = map;
+
+    ASSERT_TRUE(other.contains(3u));
+    ASSERT_TRUE(other.contains(1u));
+    ASSERT_TRUE(other.contains(11u));
+    ASSERT_FALSE(other.contains(0u));
+
+    ASSERT_EQ(other[3u], 42u);
+    ASSERT_EQ(other[1u], 99u);
+    ASSERT_EQ(other[11u], 77u);
+
+    ASSERT_EQ(other.bucket(3u), map.bucket(11u));
+    ASSERT_EQ(other.bucket(3u), other.bucket(11u));
+    ASSERT_EQ(*other.begin(3u), *map.begin(3u));
+    ASSERT_EQ(other.begin(3u)->first, 11u);
+    ASSERT_EQ((++other.begin(3u))->first, 3u);
+}
+
+TEST(DenseHashMap, Move) {
+    entt::dense_hash_map<std::size_t, std::size_t, entt::identity> map;
+    map.max_load_factor(map.max_load_factor() - .05f);
+    map.emplace(3u, 42u);
+
+    entt::dense_hash_map<std::size_t, std::size_t, entt::identity> other{std::move(map)};
+
+    ASSERT_EQ(map.size(), 0u);
+    ASSERT_TRUE(other.contains(3u));
+    ASSERT_EQ(map.max_load_factor(), other.max_load_factor());
+
+    map = other;
+    map.emplace(1u, 99u);
+    map.emplace(11u, 77u);
+    other.emplace(0u, 0u);
+    other = std::move(map);
+
+    ASSERT_EQ(map.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[3u], 42u);
+    ASSERT_EQ(other[1u], 99u);
+    ASSERT_EQ(other[11u], 77u);
+
+    ASSERT_EQ(other.bucket(3u), other.bucket(11u));
+    ASSERT_EQ(other.begin(3u)->first, 11u);
+    ASSERT_EQ((++other.begin(3u))->first, 3u);
+}
+
+TEST(DenseHashMap, Iterator) {
+    using iterator = typename entt::dense_hash_map<int, int>::iterator;
+
+    static_assert(std::is_same_v<iterator::value_type, std::pair<const int, int>>);
+    static_assert(std::is_same_v<iterator::pointer, std::pair<const int, int> *>);
+    static_assert(std::is_same_v<iterator::reference, std::pair<const int, int> &>);
+
+    entt::dense_hash_map<int, int> map;
+    map.emplace(3, 42);
+
+    iterator end{map.begin()};
+    iterator begin{};
+    begin = map.end();
+    std::swap(begin, end);
+
+    ASSERT_EQ(begin, map.begin());
+    ASSERT_EQ(end, map.end());
+    ASSERT_NE(begin, end);
+
+    ASSERT_EQ(begin++, map.begin());
+    ASSERT_EQ(begin--, map.end());
+
+    ASSERT_EQ(begin + 1, map.end());
+    ASSERT_EQ(end - 1, map.begin());
+
+    ASSERT_EQ(++begin, map.end());
+    ASSERT_EQ(--begin, map.begin());
+
+    ASSERT_EQ(begin += 1, map.end());
+    ASSERT_EQ(begin -= 1, map.begin());
+
+    ASSERT_EQ(begin + (end - begin), map.end());
+    ASSERT_EQ(begin - (begin - end), map.end());
+
+    ASSERT_EQ(end - (end - begin), map.begin());
+    ASSERT_EQ(end + (begin - end), map.begin());
+
+    ASSERT_EQ(begin[0u].first, map.begin()->first);
+    ASSERT_EQ(begin[0u].second, (*map.begin()).second);
+
+    ASSERT_LT(begin, end);
+    ASSERT_LE(begin, map.begin());
+
+    ASSERT_GT(end, begin);
+    ASSERT_GE(end, map.end());
+}
+
+TEST(DenseHashMap, ConstIterator) {
+    using iterator = typename entt::dense_hash_map<int, int>::const_iterator;
+
+    static_assert(std::is_same_v<iterator::value_type, std::pair<const int, int>>);
+    static_assert(std::is_same_v<iterator::pointer, const std::pair<const int, int> *>);
+    static_assert(std::is_same_v<iterator::reference, const std::pair<const int, int> &>);
+
+    entt::dense_hash_map<int, int> map;
+    map.emplace(3, 42);
+
+    iterator cend{map.cbegin()};
+    iterator cbegin{};
+    cbegin = map.cend();
+    std::swap(cbegin, cend);
+
+    ASSERT_EQ(cbegin, map.cbegin());
+    ASSERT_EQ(cend, map.cend());
+    ASSERT_NE(cbegin, cend);
+
+    ASSERT_EQ(cbegin++, map.cbegin());
+    ASSERT_EQ(cbegin--, map.cend());
+
+    ASSERT_EQ(cbegin + 1, map.cend());
+    ASSERT_EQ(cend - 1, map.cbegin());
+
+    ASSERT_EQ(++cbegin, map.cend());
+    ASSERT_EQ(--cbegin, map.cbegin());
+
+    ASSERT_EQ(cbegin += 1, map.cend());
+    ASSERT_EQ(cbegin -= 1, map.cbegin());
+
+    ASSERT_EQ(cbegin + (cend - cbegin), map.cend());
+    ASSERT_EQ(cbegin - (cbegin - cend), map.cend());
+
+    ASSERT_EQ(cend - (cend - cbegin), map.cbegin());
+    ASSERT_EQ(cend + (cbegin - cend), map.cbegin());
+
+    ASSERT_EQ(cbegin[0u].first, map.cbegin()->first);
+    ASSERT_EQ(cbegin[0u].second, (*map.cbegin()).second);
+    ASSERT_EQ(cbegin[0u].second, (*map.cbegin()).second);
+
+    ASSERT_LT(cbegin, cend);
+    ASSERT_LE(cbegin, map.cbegin());
+
+    ASSERT_GT(cend, cbegin);
+    ASSERT_GE(cend, map.cend());
+}
+
+TEST(DenseHashMap, IteratorConversion) {
+    entt::dense_hash_map<int, int> map;
+    map.emplace(3, 42);
+
+    typename entt::dense_hash_map<int, int>::iterator it = map.begin();
+    typename entt::dense_hash_map<int, int>::const_iterator cit = it;
+
+    static_assert(std::is_same_v<decltype(*it), std::pair<const int, int> &>);
+    static_assert(std::is_same_v<decltype(*cit), const std::pair<const int, int> &>);
+
+    ASSERT_EQ(it->first, 3);
+    ASSERT_EQ((*it).second, 42);
+    ASSERT_EQ(it->first, cit->first);
+    ASSERT_EQ((*it).second, (*it).second);
+}
+
+TEST(DenseHashMap, Insert) {
+    entt::dense_hash_map<int, int> map;
+    typename entt::dense_hash_map<int, int>::iterator it;
+    bool result;
+
+    ASSERT_TRUE(map.empty());
+    ASSERT_EQ(map.size(), 0u);
+    ASSERT_EQ(map.find(0), map.end());
+    ASSERT_FALSE(map.contains(0));
+
+    std::pair<const int, int> value{1, 2};
+    std::tie(it, result) = map.insert(std::as_const(value));
+
+    ASSERT_TRUE(result);
+    ASSERT_EQ(map.size(), 1u);
+    ASSERT_EQ(it, --map.end());
+    ASSERT_TRUE(map.contains(1));
+    ASSERT_NE(map.find(1), map.end());
+    ASSERT_EQ(it->first, 1);
+    ASSERT_EQ(it->second, 2);
+
+    value.second = 99;
+    std::tie(it, result) = map.insert(value);
+
+    ASSERT_FALSE(result);
+    ASSERT_EQ(map.size(), 1u);
+    ASSERT_EQ(it, --map.end());
+    ASSERT_EQ(it->second, 2);
+
+    std::tie(it, result) = map.insert(std::pair<const int, int>{3, 4});
+
+    ASSERT_TRUE(result);
+    ASSERT_EQ(map.size(), 2u);
+    ASSERT_EQ(it, --map.end());
+    ASSERT_TRUE(map.contains(3));
+    ASSERT_NE(map.find(3), map.end());
+    ASSERT_EQ(it->first, 3);
+    ASSERT_EQ(it->second, 4);
+
+    std::tie(it, result) = map.insert(std::pair<const int, int>{3, 99});
+
+    ASSERT_FALSE(result);
+    ASSERT_EQ(map.size(), 2u);
+    ASSERT_EQ(it, --map.end());
+    ASSERT_EQ(it->second, 4);
+
+    std::tie(it, result) = map.insert(std::pair<int, unsigned int>{5, 6u});
+
+    ASSERT_TRUE(result);
+    ASSERT_EQ(map.size(), 3u);
+    ASSERT_EQ(it, --map.end());
+    ASSERT_TRUE(map.contains(5));
+    ASSERT_NE(map.find(5), map.end());
+    ASSERT_EQ(it->first, 5);
+    ASSERT_EQ(it->second, 6);
+
+    std::tie(it, result) = map.insert(std::pair<int, unsigned int>{5, 99u});
+
+    ASSERT_FALSE(result);
+    ASSERT_EQ(map.size(), 3u);
+    ASSERT_EQ(it, --map.end());
+    ASSERT_EQ(it->second, 6);
+
+    std::pair<const int, int> range[2u]{std::make_pair(7, 8), std::make_pair(9, 10)};
+    map.insert(std::begin(range), std::end(range));
+
+    ASSERT_EQ(map.size(), 5u);
+    ASSERT_TRUE(map.contains(7));
+    ASSERT_NE(map.find(9), map.end());
+
+    range[0u].second = 99;
+    range[1u].second = 99;
+    map.insert(std::begin(range), std::end(range));
+
+    ASSERT_EQ(map.size(), 5u);
+    ASSERT_EQ(map.find(7)->second, 8);
+    ASSERT_EQ(map.find(9)->second, 10);
+}
+
+TEST(DenseHashMap, InsertRehash) {
+    static constexpr std::size_t expected_bucket_count = 8u;
+    entt::dense_hash_map<std::size_t, std::size_t, entt::identity> map;
+
+    ASSERT_EQ(map.size(), 0u);
+    ASSERT_EQ(map.bucket_count(), expected_bucket_count);
+
+    for(std::size_t next{}; next < expected_bucket_count; ++next) {
+        ASSERT_TRUE(map.insert(std::make_pair(next, next)).second);
+    }
+
+    ASSERT_EQ(map.size(), expected_bucket_count);
+    ASSERT_EQ(map.bucket_count(), expected_bucket_count);
+    ASSERT_TRUE(map.contains(expected_bucket_count / 2u));
+    ASSERT_EQ(map[expected_bucket_count - 1u], expected_bucket_count - 1u);
+    ASSERT_EQ(map.bucket(expected_bucket_count / 2u), expected_bucket_count / 2u);
+    ASSERT_FALSE(map.contains(expected_bucket_count));
+
+    ASSERT_TRUE(map.insert(std::make_pair(expected_bucket_count, expected_bucket_count)).second);
+
+    ASSERT_EQ(map.size(), expected_bucket_count + 1u);
+    ASSERT_EQ(map.bucket_count(), expected_bucket_count * 2u);
+    ASSERT_TRUE(map.contains(expected_bucket_count / 2u));
+    ASSERT_EQ(map[expected_bucket_count - 1u], expected_bucket_count - 1u);
+    ASSERT_EQ(map.bucket(expected_bucket_count / 2u), expected_bucket_count / 2u);
+    ASSERT_TRUE(map.contains(expected_bucket_count));
+
+    for(std::size_t next{}; next <= expected_bucket_count; ++next) {
+        ASSERT_TRUE(map.contains(next));
+        ASSERT_EQ(map.bucket(next), next);
+        ASSERT_EQ(map[next], next);
+    }
+}
+
+TEST(DenseHashMap, InsertSameBucket) {
+    static constexpr std::size_t expected_bucket_count = 8u;
+    entt::dense_hash_map<std::size_t, std::size_t, entt::identity> map;
+
+    for(std::size_t next{}; next < expected_bucket_count; ++next) {
+        ASSERT_EQ(map.cbegin(next), map.cend(next));
+    }
+
+    ASSERT_TRUE(map.insert(std::make_pair(1u, 1u)).second);
+    ASSERT_TRUE(map.insert(std::make_pair(9u, 9u)).second);
+
+    ASSERT_EQ(map.size(), 2u);
+    ASSERT_TRUE(map.contains(1u));
+    ASSERT_NE(map.find(9u), map.end());
+    ASSERT_EQ(map.bucket(1u), 1u);
+    ASSERT_EQ(map.bucket(9u), 1u);
+    ASSERT_EQ(map.bucket_size(1u), 2u);
+    ASSERT_EQ(map.cbegin(6u), map.cend(6u));
+}
+
+TEST(DenseHashMap, InsertOrAssign) {
+    entt::dense_hash_map<int, int> map;
+    typename entt::dense_hash_map<int, int>::iterator it;
+    bool result;
+
+    ASSERT_TRUE(map.empty());
+    ASSERT_EQ(map.size(), 0u);
+    ASSERT_EQ(map.find(0), map.end());
+    ASSERT_FALSE(map.contains(0));
+
+    const auto key = 1;
+    std::tie(it, result) = map.insert_or_assign(key, 2);
+
+    ASSERT_TRUE(result);
+    ASSERT_EQ(map.size(), 1u);
+    ASSERT_EQ(it, --map.end());
+    ASSERT_TRUE(map.contains(1));
+    ASSERT_NE(map.find(1), map.end());
+    ASSERT_EQ(it->first, 1);
+    ASSERT_EQ(it->second, 2);
+
+    std::tie(it, result) = map.insert_or_assign(key, 99);
+
+    ASSERT_FALSE(result);
+    ASSERT_EQ(map.size(), 1u);
+    ASSERT_EQ(it, --map.end());
+    ASSERT_EQ(it->second, 99);
+
+    std::tie(it, result) = map.insert_or_assign(3, 4);
+
+    ASSERT_TRUE(result);
+    ASSERT_EQ(map.size(), 2u);
+    ASSERT_EQ(it, --map.end());
+    ASSERT_TRUE(map.contains(3));
+    ASSERT_NE(map.find(3), map.end());
+    ASSERT_EQ(it->first, 3);
+    ASSERT_EQ(it->second, 4);
+
+    std::tie(it, result) = map.insert_or_assign(3, 99);
+
+    ASSERT_FALSE(result);
+    ASSERT_EQ(map.size(), 2u);
+    ASSERT_EQ(it, --map.end());
+    ASSERT_EQ(it->second, 99);
+
+    std::tie(it, result) = map.insert_or_assign(5, 6u);
+
+    ASSERT_TRUE(result);
+    ASSERT_EQ(map.size(), 3u);
+    ASSERT_EQ(it, --map.end());
+    ASSERT_TRUE(map.contains(5));
+    ASSERT_NE(map.find(5), map.end());
+    ASSERT_EQ(it->first, 5);
+    ASSERT_EQ(it->second, 6);
+
+    std::tie(it, result) = map.insert_or_assign(5, 99u);
+
+    ASSERT_FALSE(result);
+    ASSERT_EQ(map.size(), 3u);
+    ASSERT_EQ(it, --map.end());
+    ASSERT_EQ(it->second, 99);
+}
+
+TEST(DenseHashMap, Emplace) {
+    entt::dense_hash_map<int, int> map;
+    typename entt::dense_hash_map<int, int>::iterator it;
+    bool result;
+
+    ASSERT_TRUE(map.empty());
+    ASSERT_EQ(map.size(), 0u);
+    ASSERT_EQ(map.find(0), map.end());
+    ASSERT_FALSE(map.contains(0));
+
+    std::tie(it, result) = map.emplace();
+
+    ASSERT_TRUE(result);
+    ASSERT_EQ(map.size(), 1u);
+    ASSERT_EQ(it, --map.end());
+    ASSERT_TRUE(map.contains(0));
+    ASSERT_NE(map.find(0), map.end());
+    ASSERT_EQ(it->first, 0);
+    ASSERT_EQ(it->second, 0);
+
+    std::tie(it, result) = map.emplace();
+
+    ASSERT_FALSE(result);
+    ASSERT_EQ(map.size(), 1u);
+    ASSERT_EQ(it, --map.end());
+    ASSERT_EQ(it->second, 0);
+
+    std::tie(it, result) = map.emplace(std::make_pair(1, 2));
+
+    ASSERT_TRUE(result);
+    ASSERT_EQ(map.size(), 2u);
+    ASSERT_EQ(it, --map.end());
+    ASSERT_TRUE(map.contains(1));
+    ASSERT_NE(map.find(1), map.end());
+    ASSERT_EQ(it->first, 1);
+    ASSERT_EQ(it->second, 2);
+
+    std::tie(it, result) = map.emplace(std::make_pair(1, 99));
+
+    ASSERT_FALSE(result);
+    ASSERT_EQ(map.size(), 2u);
+    ASSERT_EQ(it, --map.end());
+    ASSERT_EQ(it->second, 2);
+
+    std::tie(it, result) = map.emplace(3, 4);
+
+    ASSERT_TRUE(result);
+    ASSERT_EQ(map.size(), 3u);
+    ASSERT_EQ(it, --map.end());
+    ASSERT_TRUE(map.contains(3));
+    ASSERT_NE(map.find(3), map.end());
+    ASSERT_EQ(it->first, 3);
+    ASSERT_EQ(it->second, 4);
+
+    std::tie(it, result) = map.emplace(3, 99);
+
+    ASSERT_FALSE(result);
+    ASSERT_EQ(map.size(), 3u);
+    ASSERT_EQ(it, --map.end());
+    ASSERT_EQ(it->second, 4);
+
+    std::tie(it, result) = map.emplace(std::piecewise_construct, std::make_tuple(5), std::make_tuple(6u));
+
+    ASSERT_TRUE(result);
+    ASSERT_EQ(map.size(), 4u);
+    ASSERT_EQ(it, --map.end());
+    ASSERT_TRUE(map.contains(5));
+    ASSERT_NE(map.find(5), map.end());
+    ASSERT_EQ(it->first, 5);
+    ASSERT_EQ(it->second, 6);
+
+    std::tie(it, result) = map.emplace(std::piecewise_construct, std::make_tuple(5), std::make_tuple(99u));
+
+    ASSERT_FALSE(result);
+    ASSERT_EQ(map.size(), 4u);
+    ASSERT_EQ(it, --map.end());
+    ASSERT_EQ(it->second, 6);
+
+    std::tie(it, result) = map.emplace(std::make_pair(1, 99));
+
+    ASSERT_FALSE(result);
+    ASSERT_EQ(map.size(), 4u);
+    ASSERT_EQ(it, ++map.begin());
+    ASSERT_EQ(it->second, 2);
+}
+
+TEST(DenseHashMap, EmplaceRehash) {
+    static constexpr std::size_t expected_bucket_count = 8u;
+    entt::dense_hash_map<std::size_t, std::size_t, entt::identity> map;
+
+    ASSERT_EQ(map.size(), 0u);
+    ASSERT_EQ(map.bucket_count(), expected_bucket_count);
+
+    for(std::size_t next{}; next < expected_bucket_count; ++next) {
+        ASSERT_TRUE(map.emplace(next, next).second);
+    }
+
+    ASSERT_EQ(map.size(), expected_bucket_count);
+    ASSERT_EQ(map.bucket_count(), expected_bucket_count);
+    ASSERT_TRUE(map.contains(expected_bucket_count / 2u));
+    ASSERT_EQ(map[expected_bucket_count - 1u], expected_bucket_count - 1u);
+    ASSERT_EQ(map.bucket(expected_bucket_count / 2u), expected_bucket_count / 2u);
+    ASSERT_FALSE(map.contains(expected_bucket_count));
+
+    ASSERT_TRUE(map.emplace(expected_bucket_count, expected_bucket_count).second);
+
+    ASSERT_EQ(map.size(), expected_bucket_count + 1u);
+    ASSERT_EQ(map.bucket_count(), expected_bucket_count * 2u);
+    ASSERT_TRUE(map.contains(expected_bucket_count / 2u));
+    ASSERT_EQ(map[expected_bucket_count - 1u], expected_bucket_count - 1u);
+    ASSERT_EQ(map.bucket(expected_bucket_count / 2u), expected_bucket_count / 2u);
+    ASSERT_TRUE(map.contains(expected_bucket_count));
+
+    for(std::size_t next{}; next <= expected_bucket_count; ++next) {
+        ASSERT_TRUE(map.contains(next));
+        ASSERT_EQ(map.bucket(next), next);
+        ASSERT_EQ(map[next], next);
+    }
+}
+
+TEST(DenseHashMap, EmplaceSameBucket) {
+    static constexpr std::size_t expected_bucket_count = 8u;
+    entt::dense_hash_map<std::size_t, std::size_t, entt::identity> map;
+
+    for(std::size_t next{}; next < expected_bucket_count; ++next) {
+        ASSERT_EQ(map.cbegin(next), map.cend(next));
+    }
+
+    ASSERT_TRUE(map.emplace(1u, 1u).second);
+    ASSERT_TRUE(map.emplace(9u, 9u).second);
+
+    ASSERT_EQ(map.size(), 2u);
+    ASSERT_TRUE(map.contains(1u));
+    ASSERT_NE(map.find(9u), map.end());
+    ASSERT_EQ(map.bucket(1u), 1u);
+    ASSERT_EQ(map.bucket(9u), 1u);
+    ASSERT_EQ(map.bucket_size(1u), 2u);
+    ASSERT_EQ(map.cbegin(6u), map.cend(6u));
+}
+
+TEST(DenseHashMap, TryEmplace) {
+    entt::dense_hash_map<int, int> map;
+    typename entt::dense_hash_map<int, int>::iterator it;
+    bool result;
+
+    ASSERT_TRUE(map.empty());
+    ASSERT_EQ(map.size(), 0u);
+    ASSERT_EQ(map.find(1), map.end());
+    ASSERT_FALSE(map.contains(1));
+
+    std::tie(it, result) = map.try_emplace(1, 2);
+
+    ASSERT_TRUE(result);
+    ASSERT_EQ(map.size(), 1u);
+    ASSERT_EQ(it, --map.end());
+    ASSERT_TRUE(map.contains(1));
+    ASSERT_NE(map.find(1), map.end());
+    ASSERT_EQ(it->first, 1);
+    ASSERT_EQ(it->second, 2);
+
+    std::tie(it, result) = map.try_emplace(1, 99);
+
+    ASSERT_FALSE(result);
+    ASSERT_EQ(map.size(), 1u);
+    ASSERT_EQ(it, --map.end());
+    ASSERT_EQ(it->second, 2);
+}
+
+TEST(DenseHashMap, TryEmplaceRehash) {
+    static constexpr std::size_t expected_bucket_count = 8u;
+    entt::dense_hash_map<std::size_t, std::size_t, entt::identity> map;
+
+    ASSERT_EQ(map.size(), 0u);
+    ASSERT_EQ(map.bucket_count(), expected_bucket_count);
+
+    for(std::size_t next{}; next < expected_bucket_count; ++next) {
+        ASSERT_TRUE(map.try_emplace(next, next).second);
+    }
+
+    ASSERT_EQ(map.size(), expected_bucket_count);
+    ASSERT_EQ(map.bucket_count(), expected_bucket_count);
+    ASSERT_TRUE(map.contains(expected_bucket_count / 2u));
+    ASSERT_EQ(map[expected_bucket_count - 1u], expected_bucket_count - 1u);
+    ASSERT_EQ(map.bucket(expected_bucket_count / 2u), expected_bucket_count / 2u);
+    ASSERT_FALSE(map.contains(expected_bucket_count));
+
+    ASSERT_TRUE(map.try_emplace(expected_bucket_count, expected_bucket_count).second);
+
+    ASSERT_EQ(map.size(), expected_bucket_count + 1u);
+    ASSERT_EQ(map.bucket_count(), expected_bucket_count * 2u);
+    ASSERT_TRUE(map.contains(expected_bucket_count / 2u));
+    ASSERT_EQ(map[expected_bucket_count - 1u], expected_bucket_count - 1u);
+    ASSERT_EQ(map.bucket(expected_bucket_count / 2u), expected_bucket_count / 2u);
+    ASSERT_TRUE(map.contains(expected_bucket_count));
+
+    for(std::size_t next{}; next <= expected_bucket_count; ++next) {
+        ASSERT_TRUE(map.contains(next));
+        ASSERT_EQ(map.bucket(next), next);
+        ASSERT_EQ(map[next], next);
+    }
+}
+
+TEST(DenseHashMap, TryEmplaceSameBucket) {
+    static constexpr std::size_t expected_bucket_count = 8u;
+    entt::dense_hash_map<std::size_t, std::size_t, entt::identity> map;
+
+    for(std::size_t next{}; next < expected_bucket_count; ++next) {
+        ASSERT_EQ(map.cbegin(next), map.cend(next));
+    }
+
+    ASSERT_TRUE(map.try_emplace(1u, 1u).second);
+    ASSERT_TRUE(map.try_emplace(9u, 9u).second);
+
+    ASSERT_EQ(map.size(), 2u);
+    ASSERT_TRUE(map.contains(1u));
+    ASSERT_NE(map.find(9u), map.end());
+    ASSERT_EQ(map.bucket(1u), 1u);
+    ASSERT_EQ(map.bucket(9u), 1u);
+    ASSERT_EQ(map.bucket_size(1u), 2u);
+    ASSERT_EQ(map.cbegin(6u), map.cend(6u));
+}
+
+TEST(DenseHashMap, Erase) {
+    static constexpr std::size_t expected_bucket_count = 8u;
+    entt::dense_hash_map<std::size_t, std::size_t, entt::identity> map;
+
+    for(std::size_t next{}, last = expected_bucket_count + 1u; next < last; ++next) {
+        map.emplace(next, next);
+    }
+
+    ASSERT_EQ(map.bucket_count(), 2 * expected_bucket_count);
+    ASSERT_EQ(map.size(), expected_bucket_count + 1u);
+
+    for(std::size_t next{}, last = expected_bucket_count + 1u; next < last; ++next) {
+        ASSERT_TRUE(map.contains(next));
+    }
+
+    auto it = map.erase(++map.begin());
+    it = map.erase(it, it + 1);
+
+    ASSERT_EQ((--map.end())->first, 6u);
+    ASSERT_EQ(map.erase(6u), 1u);
+    ASSERT_EQ(map.erase(6u), 0u);
+
+    ASSERT_EQ(map.bucket_count(), 2 * expected_bucket_count);
+    ASSERT_EQ(map.size(), expected_bucket_count + 1u - 3u);
+
+    ASSERT_EQ(it, ++map.begin());
+    ASSERT_EQ(it->first, 7u);
+    ASSERT_EQ((--map.end())->first, 5u);
+
+    for(std::size_t next{}, last = expected_bucket_count + 1u; next < last; ++next) {
+        if(next == 1u || next == 8u || next == 6u) {
+            ASSERT_FALSE(map.contains(next));
+            ASSERT_EQ(map.bucket_size(next), 0u);
+        } else {
+            ASSERT_TRUE(map.contains(next));
+            ASSERT_EQ(map.bucket(next), next);
+            ASSERT_EQ(map.bucket_size(next), 1u);
+        }
+    }
+
+    map.erase(map.begin(), map.end());
+
+    for(std::size_t next{}, last = expected_bucket_count + 1u; next < last; ++next) {
+        ASSERT_FALSE(map.contains(next));
+    }
+
+    ASSERT_EQ(map.bucket_count(), 2 * expected_bucket_count);
+    ASSERT_EQ(map.size(), 0u);
+}
+
+TEST(DenseHashMap, EraseFromBucket) {
+    static constexpr std::size_t expected_bucket_count = 8u;
+    entt::dense_hash_map<std::size_t, std::size_t, entt::identity> map;
+
+    ASSERT_EQ(map.bucket_count(), expected_bucket_count);
+    ASSERT_EQ(map.size(), 0u);
+
+    for(std::size_t next{}; next < 4u; ++next) {
+        ASSERT_TRUE(map.emplace(2u * expected_bucket_count * next, 2u * 2u * expected_bucket_count * next).second);
+        ASSERT_TRUE(map.emplace(2u * expected_bucket_count * next + 2u, 2u * expected_bucket_count * next + 2u).second);
+        ASSERT_TRUE(map.emplace(2u * expected_bucket_count * (next + 1u) - 1u, 2u * expected_bucket_count * (next + 1u) - 1u).second);
+    }
+
+    ASSERT_EQ(map.bucket_count(), 2u * expected_bucket_count);
+    ASSERT_EQ(map.size(), 12u);
+
+    ASSERT_EQ(map.bucket_size(0u), 4u);
+    ASSERT_EQ(map.bucket_size(2u), 4u);
+    ASSERT_EQ(map.bucket_size(15u), 4u);
+
+    map.erase(map.end() - 3, map.end());
+
+    ASSERT_EQ(map.bucket_count(), 2u * expected_bucket_count);
+    ASSERT_EQ(map.size(), 9u);
+
+    ASSERT_EQ(map.bucket_size(0u), 3u);
+    ASSERT_EQ(map.bucket_size(2u), 3u);
+    ASSERT_EQ(map.bucket_size(15u), 3u);
+
+    for(std::size_t next{}; next < 3u; ++next) {
+        ASSERT_TRUE(map.contains(2u * expected_bucket_count * next));
+        ASSERT_EQ(map.bucket(2u * expected_bucket_count * next), 0u);
+
+        ASSERT_TRUE(map.contains(2u * expected_bucket_count * next + 2u));
+        ASSERT_EQ(map.bucket(2u * expected_bucket_count * next + 2u), 2u);
+
+        ASSERT_TRUE(map.contains(2u * expected_bucket_count * (next + 1u) - 1u));
+        ASSERT_EQ(map.bucket(2u * expected_bucket_count * (next + 1u) - 1u), 15u);
+    }
+
+    ASSERT_FALSE(map.contains(2u * expected_bucket_count * 3u));
+    ASSERT_FALSE(map.contains(2u * expected_bucket_count * 3u + 2u));
+    ASSERT_FALSE(map.contains(2u * expected_bucket_count * (3u + 1u) - 1u));
+
+    map.erase((++map.begin(0u))->first);
+    map.erase((++map.begin(2u))->first);
+    map.erase((++map.begin(15u))->first);
+
+    ASSERT_EQ(map.bucket_count(), 2u * expected_bucket_count);
+    ASSERT_EQ(map.size(), 6u);
+
+    ASSERT_EQ(map.bucket_size(0u), 2u);
+    ASSERT_EQ(map.bucket_size(2u), 2u);
+    ASSERT_EQ(map.bucket_size(15u), 2u);
+
+    ASSERT_FALSE(map.contains(2u * expected_bucket_count * 1u));
+    ASSERT_FALSE(map.contains(2u * expected_bucket_count * 1u + 2u));
+    ASSERT_FALSE(map.contains(2u * expected_bucket_count * (1u + 1u) - 1u));
+
+    while(map.begin(15) != map.end(15u)) {
+        map.erase(map.begin(15)->first);
+    }
+
+    ASSERT_EQ(map.bucket_count(), 2u * expected_bucket_count);
+    ASSERT_EQ(map.size(), 4u);
+
+    ASSERT_EQ(map.bucket_size(0u), 2u);
+    ASSERT_EQ(map.bucket_size(2u), 2u);
+    ASSERT_EQ(map.bucket_size(15u), 0u);
+
+    ASSERT_TRUE(map.contains(0u * expected_bucket_count));
+    ASSERT_TRUE(map.contains(0u * expected_bucket_count + 2u));
+    ASSERT_TRUE(map.contains(4u * expected_bucket_count));
+    ASSERT_TRUE(map.contains(4u * expected_bucket_count + 2u));
+
+    map.erase(4u * expected_bucket_count + 2u);
+    map.erase(0u * expected_bucket_count);
+
+    ASSERT_EQ(map.bucket_count(), 2u * expected_bucket_count);
+    ASSERT_EQ(map.size(), 2u);
+
+    ASSERT_EQ(map.bucket_size(0u), 1u);
+    ASSERT_EQ(map.bucket_size(2u), 1u);
+    ASSERT_EQ(map.bucket_size(15u), 0u);
+
+    ASSERT_FALSE(map.contains(0u * expected_bucket_count));
+    ASSERT_TRUE(map.contains(0u * expected_bucket_count + 2u));
+    ASSERT_TRUE(map.contains(4u * expected_bucket_count));
+    ASSERT_FALSE(map.contains(4u * expected_bucket_count + 2u));
+}
+
+TEST(DenseHashMap, Swap) {
+    entt::dense_hash_map<int, int> map;
+    entt::dense_hash_map<int, int> other;
+
+    map.emplace(0, 1);
+
+    ASSERT_FALSE(map.empty());
+    ASSERT_TRUE(other.empty());
+    ASSERT_TRUE(map.contains(0));
+    ASSERT_FALSE(other.contains(0));
+
+    map.swap(other);
+
+    ASSERT_TRUE(map.empty());
+    ASSERT_FALSE(other.empty());
+    ASSERT_FALSE(map.contains(0));
+    ASSERT_TRUE(other.contains(0));
+}
+
+TEST(DenseHashMap, Indexing) {
+    entt::dense_hash_map<int, int> map;
+    const auto key = 1;
+
+    ASSERT_FALSE(map.contains(key));
+    ASSERT_DEATH([[maybe_unused]] auto value = std::as_const(map).at(key), "");
+    ASSERT_DEATH([[maybe_unused]] auto value = map.at(key), "");
+
+    map[key] = 99;
+
+    ASSERT_TRUE(map.contains(key));
+    ASSERT_EQ(map[std::move(key)], 99);
+    ASSERT_EQ(std::as_const(map).at(key), 99);
+    ASSERT_EQ(map.at(key), 99);
+}
+
+TEST(DenseHashMap, LocalIterator) {
+    using iterator = typename entt::dense_hash_map<std::size_t, std::size_t, entt::identity>::local_iterator;
+
+    static_assert(std::is_same_v<iterator::value_type, std::pair<const std::size_t, std::size_t>>);
+    static_assert(std::is_same_v<iterator::pointer, std::pair<const std::size_t, std::size_t> *>);
+    static_assert(std::is_same_v<iterator::reference, std::pair<const std::size_t, std::size_t> &>);
+
+    static constexpr std::size_t expected_bucket_count = 8u;
+    entt::dense_hash_map<std::size_t, std::size_t, entt::identity> map;
+    map.emplace(3u, 42u);
+    map.emplace(3u + expected_bucket_count, 99u);
+
+    iterator end{map.begin(3u)};
+    iterator begin{};
+    begin = map.end(3u);
+    std::swap(begin, end);
+
+    ASSERT_EQ(begin, map.begin(3u));
+    ASSERT_EQ(end, map.end(3u));
+    ASSERT_NE(begin, end);
+
+    ASSERT_EQ(begin->first, 3u + expected_bucket_count);
+    ASSERT_EQ((*begin).second, 99u);
+
+    ASSERT_EQ(begin.index(), 1u);
+    ASSERT_EQ(begin++, map.begin(3u));
+    ASSERT_EQ(begin.index(), 0u);
+    ASSERT_EQ(++begin, map.end(3u));
+    ASSERT_GT(begin.index(), map.size());
+}
+
+TEST(DenseHashMap, ConstLocalIterator) {
+    using iterator = typename entt::dense_hash_map<std::size_t, std::size_t, entt::identity>::const_local_iterator;
+
+    static_assert(std::is_same_v<iterator::value_type, std::pair<const std::size_t, std::size_t>>);
+    static_assert(std::is_same_v<iterator::pointer, const std::pair<const std::size_t, std::size_t> *>);
+    static_assert(std::is_same_v<iterator::reference, const std::pair<const std::size_t, std::size_t> &>);
+
+    static constexpr std::size_t expected_bucket_count = 8u;
+    entt::dense_hash_map<std::size_t, std::size_t, entt::identity> map;
+    map.emplace(3u, 42u);
+    map.emplace(3u + expected_bucket_count, 99u);
+
+    iterator cend{map.begin(3u)};
+    iterator cbegin{};
+    cbegin = map.end(3u);
+    std::swap(cbegin, cend);
+
+    ASSERT_EQ(cbegin, map.begin(3u));
+    ASSERT_EQ(cend, map.end(3u));
+    ASSERT_NE(cbegin, cend);
+
+    ASSERT_EQ(cbegin->first, 3u + expected_bucket_count);
+    ASSERT_EQ((*cbegin).second, 99u);
+
+    ASSERT_EQ(cbegin.index(), 1u);
+    ASSERT_EQ(cbegin++, map.begin(3u));
+    ASSERT_EQ(cbegin.index(), 0u);
+    ASSERT_EQ(++cbegin, map.end(3u));
+    ASSERT_GT(cbegin.index(), map.size());
+}
+
+TEST(DenseHashMap, LocalIteratorConversion) {
+    entt::dense_hash_map<int, int> map;
+    map.emplace(3, 42);
+
+    typename entt::dense_hash_map<int, int>::local_iterator it = map.begin(map.bucket(3));
+    typename entt::dense_hash_map<int, int>::const_local_iterator cit = it;
+
+    static_assert(std::is_same_v<decltype(*it), std::pair<const int, int> &>);
+    static_assert(std::is_same_v<decltype(*cit), const std::pair<const int, int> &>);
+
+    ASSERT_EQ(it->first, 3);
+    ASSERT_EQ((*it).second, 42);
+    ASSERT_EQ(it->first, cit->first);
+    ASSERT_EQ((*it).second, (*it).second);
+}
+
+TEST(DenseHashMap, Rehash) {
+    static constexpr std::size_t expected_bucket_count = 8u;
+    entt::dense_hash_map<std::size_t, std::size_t, entt::identity> map;
+    map[32u] = 99u;
+
+    ASSERT_EQ(map.bucket_count(), expected_bucket_count);
+    ASSERT_TRUE(map.contains(32u));
+    ASSERT_EQ(map.bucket(32u), 0u);
+    ASSERT_EQ(map[32u], 99u);
+
+    map.rehash(12u);
+
+    ASSERT_EQ(map.bucket_count(), 2u * expected_bucket_count);
+    ASSERT_TRUE(map.contains(32u));
+    ASSERT_EQ(map.bucket(32u), 0u);
+    ASSERT_EQ(map[32u], 99u);
+
+    map.rehash(44u);
+
+    ASSERT_EQ(map.bucket_count(), 8u * expected_bucket_count);
+    ASSERT_TRUE(map.contains(32u));
+    ASSERT_EQ(map.bucket(32u), 32u);
+    ASSERT_EQ(map[32u], 99u);
+
+    map.rehash(0u);
+
+    ASSERT_EQ(map.bucket_count(), expected_bucket_count);
+    ASSERT_TRUE(map.contains(32u));
+    ASSERT_EQ(map.bucket(32u), 0u);
+    ASSERT_EQ(map[32u], 99u);
+
+    for(std::size_t next{}; next < expected_bucket_count; ++next) {
+        map.emplace(next, next);
+    }
+
+    ASSERT_EQ(map.size(), expected_bucket_count + 1u);
+    ASSERT_EQ(map.bucket_count(), 2u * expected_bucket_count);
+
+    map.rehash(0u);
+
+    ASSERT_EQ(map.bucket_count(), 2u * expected_bucket_count);
+    ASSERT_TRUE(map.contains(32u));
+
+    map.rehash(55u);
+
+    ASSERT_EQ(map.bucket_count(), 8u * expected_bucket_count);
+    ASSERT_TRUE(map.contains(32u));
+
+    map.rehash(2u);
+
+    ASSERT_EQ(map.bucket_count(), 2u * expected_bucket_count);
+    ASSERT_TRUE(map.contains(32u));
+    ASSERT_EQ(map.bucket(32u), 0u);
+    ASSERT_EQ(map[32u], 99u);
+
+    for(std::size_t next{}; next < expected_bucket_count; ++next) {
+        ASSERT_TRUE(map.contains(next));
+        ASSERT_EQ(map[next], next);
+        ASSERT_EQ(map.bucket(next), next);
+    }
+
+    ASSERT_EQ(map.bucket_size(0u), 2u);
+    ASSERT_EQ(map.bucket_size(3u), 1u);
+
+    ASSERT_EQ(map.begin(0u)->first, 0u);
+    ASSERT_EQ(map.begin(0u)->second, 0u);
+    ASSERT_EQ((++map.begin(0u))->first, 32u);
+    ASSERT_EQ((++map.begin(0u))->second, 99u);
+
+    map.clear();
+    map.rehash(2u);
+
+    ASSERT_EQ(map.bucket_count(), expected_bucket_count);
+    ASSERT_FALSE(map.contains(32u));
+
+    for(std::size_t next{}; next < expected_bucket_count; ++next) {
+        ASSERT_FALSE(map.contains(next));
+    }
+
+    ASSERT_EQ(map.bucket_size(0u), 0u);
+    ASSERT_EQ(map.bucket_size(3u), 0u);
+}
+
+TEST(DenseHashMap, Reserve) {
+    static constexpr std::size_t expected_bucket_count = 8u;
+    entt::dense_hash_map<int, int> map;
+
+    ASSERT_EQ(map.bucket_count(), expected_bucket_count);
+
+    map.reserve(0u);
+
+    ASSERT_EQ(map.bucket_count(), expected_bucket_count);
+
+    map.reserve(expected_bucket_count);
+
+    ASSERT_EQ(map.bucket_count(), 2 * expected_bucket_count);
+    ASSERT_EQ(map.bucket_count(), entt::next_power_of_two(std::ceil(expected_bucket_count / map.max_load_factor())));
+}