any: ::hash function for hashable types (close #629)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user