Просмотр исходного кода

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

Michele Caini 4 лет назад
Родитель
Сommit
fa8362f000
3 измененных файлов с 100 добавлено и 11 удалено
  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)
 * [Any as in any type](#any-as-in-any-type)
+  * [Hashing of any objects](#hashing-of-any-objects)
   * [Small buffer optimization](#small-buffer-optimization)
   * [Alignment requirement](#alignment-requirement)
 * [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
 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
 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/>
@@ -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
 element when required, regardless of the storage strategy used for the specific
 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
 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
 thus created won't create copies and will only serve as a reference for the
 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
 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
 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
 
 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
 
 #include <cstddef>
+#include <functional>
 #include <memory>
 #include <type_traits>
 #include <utility>
@@ -27,6 +28,7 @@ class basic_any {
         assign,
         destroy,
         compare,
+        hash,
         get
     };
 
@@ -94,6 +96,12 @@ class basic_any {
             } else {
                 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:
             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.
      * @return True in case of success, false otherwise.
      */
@@ -314,11 +322,7 @@ public:
         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) {
         if(vtable && mode != policy::cref && *info == *other.info) {
             if(auto *val = other.data(); val) {
@@ -384,6 +388,23 @@ public:
         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:
     union {
         const void *instance;
@@ -491,4 +512,21 @@ basic_any<Len, Align> forward_as_any(Type &&value) {
 
 } // 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

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

@@ -1171,6 +1171,33 @@ TEST_F(Any, CompareVoid) {
     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) {
     entt::any any{42};
     const auto &cany = any;