Browse Source

any: ::hash function for hashable types (close #629)

Michele Caini 4 years ago
parent
commit
fa8362f000
3 changed files with 100 additions and 11 deletions
  1. 29 5
      docs/md/core.md
  2. 44 6
      src/entt/core/any.hpp
  3. 27 0
      test/entt/core/any.cpp

+ 29 - 5
docs/md/core.md

@@ -7,6 +7,7 @@
 
 
 * [Introduction](#introduction)
 * [Introduction](#introduction)
 * [Any as in any type](#any-as-in-any-type)
 * [Any as in any type](#any-as-in-any-type)
+  * [Hashing of any objects](#hashing-of-any-objects)
   * [Small buffer optimization](#small-buffer-optimization)
   * [Small buffer optimization](#small-buffer-optimization)
   * [Alignment requirement](#alignment-requirement)
   * [Alignment requirement](#alignment-requirement)
 * [Compressed pair](#compressed-pair)
 * [Compressed pair](#compressed-pair)
@@ -49,7 +50,7 @@ describing what `EnTT` offers so as not to reinvent the wheel in case of need.
 
 
 `EnTT` comes with its own `any` type. It may seem redundant considering that
 `EnTT` comes with its own `any` type. It may seem redundant considering that
 C++17 introduced `std::any`, but it is not (hopefully).<br/>
 C++17 introduced `std::any`, but it is not (hopefully).<br/>
-In fact, the _type_ returned by an `std::any` is a const reference to an
+First of all, the _type_ returned by an `std::any` is a const reference to an
 `std::type_info`, an implementation defined class that's not something everyone
 `std::type_info`, an implementation defined class that's not something everyone
 wants to see in a software. Furthermore, there is no way to connect it with the
 wants to see in a software. Furthermore, there is no way to connect it with the
 type system of the library and therefore with its integrated RTTI support.<br/>
 type system of the library and therefore with its integrated RTTI support.<br/>
@@ -84,9 +85,9 @@ entt::any any = entt::make_any<int>(42);
 In both cases, the `any` class takes the burden of destroying the contained
 In both cases, the `any` class takes the burden of destroying the contained
 element when required, regardless of the storage strategy used for the specific
 element when required, regardless of the storage strategy used for the specific
 object.<br/>
 object.<br/>
-Furthermore, an instance of `any` is not tied to an actual type. Therefore, the
-wrapper will be reconfigured by assigning it an object of a different type than
-the one contained, so as to be able to handle the new instance.
+Furthermore, an instance of `any` isn't tied to an actual type. Therefore, the
+wrapper is reconfigured when it's assigned a new object of a type other than
+the one it contains.
 
 
 There exists also a way to directly assign a value to the variable contained by
 There exists also a way to directly assign a value to the variable contained by
 an `entt::any`, without necessarily replacing it. This is especially useful when
 an `entt::any`, without necessarily replacing it. This is especially useful when
@@ -151,7 +152,7 @@ In this case, it doesn't matter if the original container actually holds an
 object or acts already as a reference for unmanaged elements, the new instance
 object or acts already as a reference for unmanaged elements, the new instance
 thus created won't create copies and will only serve as a reference for the
 thus created won't create copies and will only serve as a reference for the
 original item.<br/>
 original item.<br/>
-This means that, starting from the example above, both `ref` and` other` will
+This means that, starting from the example above, both `ref` and `other` will
 point to the same object, whether it's initially contained in `other` or already
 point to the same object, whether it's initially contained in `other` or already
 an unmanaged element.
 an unmanaged element.
 
 
@@ -167,6 +168,29 @@ The only difference is that, in the case of `EnTT`, these won't raise exceptions
 but will only trigger an assert in debug mode, otherwise resulting in undefined
 but will only trigger an assert in debug mode, otherwise resulting in undefined
 behavior in case of misuse in release mode.
 behavior in case of misuse in release mode.
 
 
+## Hashing of any objects
+
+As for the `any` class, the hashing topic deserves a section of its own.<br/>
+It's indeed possible to extract the hash value (as in `std::hash`) of an object
+managed by `any`:
+
+```cpp
+const std::size_t hash = any.hash();
+```
+
+However, there are some limitations:
+
+* The instance of `any` **must** not be empty, otherwise the returned value is
+  that of `std::hash<std::nullptr_t>{}({})`.
+
+* The underlying object **must** support this operation, otherwise the returned
+  value is that of `std::hash<std::nullptr_t>{}({})`.
+
+Unfortunately, it's not possible to trigger a compile-time error in these cases.
+This would prevent users from using non-hashable types with `any`.<br/>
+A compromise has therefore been made that could change over time but which
+appears to be acceptable today for the conceivable uses of this feature.
+
 ## Small buffer optimization
 ## Small buffer optimization
 
 
 The `any` class uses a technique called _small buffer optimization_ to reduce
 The `any` class uses a technique called _small buffer optimization_ to reduce

+ 44 - 6
src/entt/core/any.hpp

@@ -2,6 +2,7 @@
 #define ENTT_CORE_ANY_HPP
 #define ENTT_CORE_ANY_HPP
 
 
 #include <cstddef>
 #include <cstddef>
+#include <functional>
 #include <memory>
 #include <memory>
 #include <type_traits>
 #include <type_traits>
 #include <utility>
 #include <utility>
@@ -27,6 +28,7 @@ class basic_any {
         assign,
         assign,
         destroy,
         destroy,
         compare,
         compare,
+        hash,
         get
         get
     };
     };
 
 
@@ -94,6 +96,12 @@ class basic_any {
             } else {
             } else {
                 return (element == other) ? other : nullptr;
                 return (element == other) ? other : nullptr;
             }
             }
+        case operation::hash:
+            if constexpr(is_std_hashable_v<Type>) {
+                *static_cast<std::size_t *>(const_cast<void *>(other)) = std::hash<Type>{}(*element);
+                return element;
+            }
+            break;
         case operation::get:
         case operation::get:
             return element;
             return element;
         }
         }
@@ -302,7 +310,7 @@ public:
     }
     }
 
 
     /**
     /**
-     * @brief Copy assigns a value to the contained object without replacing it.
+     * @brief Assigns a value to the contained object without replacing it.
      * @param other The value to assign to the contained object.
      * @param other The value to assign to the contained object.
      * @return True in case of success, false otherwise.
      * @return True in case of success, false otherwise.
      */
      */
@@ -314,11 +322,7 @@ public:
         return false;
         return false;
     }
     }
 
 
-    /**
-     * @brief Move assigns a value to the contained object without replacing it.
-     * @param other The value to assign to the contained object.
-     * @return True in case of success, false otherwise.
-     */
+    /*! @copydoc assign */
     bool assign(any &&other) {
     bool assign(any &&other) {
         if(vtable && mode != policy::cref && *info == *other.info) {
         if(vtable && mode != policy::cref && *info == *other.info) {
             if(auto *val = other.data(); val) {
             if(auto *val = other.data(); val) {
@@ -384,6 +388,23 @@ public:
         return (mode == policy::owner);
         return (mode == policy::owner);
     }
     }
 
 
+    /**
+     * @brief Returns the hash value of the contained object.
+     *
+     * If the underlying object isn't _hashable_, the hash of its address is
+     * returned once converted to `const void *`.
+     *
+     * @return The hash value of the contained object or its address if any,
+     * `std::hash<std::nullptr_t>{}({})` otherwise.
+     */
+    [[nodiscard]] std::size_t hash() const ENTT_NOEXCEPT {
+        if(std::size_t value{}; vtable && vtable(operation::hash, *this, &value)) {
+            return value;
+        }
+
+        return std::hash<std::nullptr_t>{}({});
+    }
+
 private:
 private:
     union {
     union {
         const void *instance;
         const void *instance;
@@ -491,4 +512,21 @@ basic_any<Len, Align> forward_as_any(Type &&value) {
 
 
 } // namespace entt
 } // namespace entt
 
 
+namespace std {
+
+/*! @brief `std::hash` specialization for `entt::any`. */
+template<>
+struct hash<entt::any> {
+    /**
+     * @brief Returns the hash value of the parameter.
+     * @param any The object to return the hash for.
+     * @return The hash value of the parameter.
+     */
+    [[nodiscard]] std::size_t operator()(const entt::any &any) const ENTT_NOEXCEPT {
+        return any.hash();
+    }
+};
+
+} // namespace std
+
 #endif
 #endif

+ 27 - 0
test/entt/core/any.cpp

@@ -1171,6 +1171,33 @@ TEST_F(Any, CompareVoid) {
     ASSERT_FALSE(entt::any{} != any);
     ASSERT_FALSE(entt::any{} != any);
 }
 }
 
 
+TEST_F(Any, Hashable) {
+    const int value = 42;
+    entt::any any{value};
+    const entt::any ref{std::in_place_type<const int &>, value};
+
+    ASSERT_TRUE(any);
+    ASSERT_TRUE(ref);
+
+    ASSERT_EQ(any.hash(), std::hash<int>{}(value));
+    ASSERT_EQ(std::hash<int>{}(value), std::hash<entt::any>{}(ref));
+    ASSERT_EQ(ref.hash(), std::hash<entt::any>{}(any));
+}
+
+TEST_F(Any, NotHashable) {
+    const not_comparable value{};
+    entt::any any{value};
+    const entt::any ref{std::in_place_type<const not_comparable &>, value};
+
+    ASSERT_TRUE(any);
+    ASSERT_TRUE(ref);
+
+    ASSERT_EQ(any.hash(), std::hash<std::nullptr_t>{}({}));
+    ASSERT_EQ(std::hash<std::nullptr_t>{}({}), std::hash<entt::any>{}(ref));
+    ASSERT_EQ(ref.hash(), std::hash<entt::any>{}(any));
+    ASSERT_EQ(any.hash(), entt::any{}.hash());
+}
+
 TEST_F(Any, AnyCast) {
 TEST_F(Any, AnyCast) {
     entt::any any{42};
     entt::any any{42};
     const auto &cany = any;
     const auto &cany = any;