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

This commit is contained in:
Michele Caini
2022-02-08 10:55:40 +01:00
parent 1816206dfc
commit fa8362f000
3 changed files with 100 additions and 11 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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;