I will agree with the other answers that it's not generally possible as-stated in standard C++ yet, but we may solve a constrained version of the problem.
Since this is all compile-time programming, we cannot have mutable state, so if you're willing to use a new variable for each state change, then something like this is possible:
- hash_state1 = hash(type1)
- hash_state2 = hash(type2, hash_state1)
- hash_state3 = hash(type3, hash_state2)
Where "hash_state" is really just a unique typelist of all the types we've hashed so far. It can also provide a size_t
value as a result of hashing a new type.
If a type that we seek to hash is already present in the typelist, we return the index of that type.
This requires quite a bit of boilerplate:
- Ensuring types are unique within a typelist: I used @Deduplicator's answer here: https://mcmap.net/q/1007419/-how-to-define-a-variant-lt-x-y-z-gt-extracting-subtypes-of-a-template-parameter
- Finding a type in a unique typelist
- Using
if constexpr
to check if a type is in the typelist (C++17)
Part 1: a unique typelist:
Again, all credit to @Deduplicator's answer here on this part. The following code saves compile-time performance by doing lookups on a typelist in O(log N) time thanks to leaning on the implementation of tuple-cat.
The code is written almost frustratingly generically, but the nice part is that it allows you to work with any generic typelist (tuple
, variant
, something custom).
namespace detail {
template <template <class...> class TT, template <class...> class UU, class... Us>
auto pack(UU<Us...>)
-> std::tuple<TT<Us>...>;
template <template <class...> class TT, class... Ts>
auto unpack(std::tuple<TT<Ts>...>)
-> TT<Ts...>;
template <std::size_t N, class T>
using TET = std::tuple_element_t<N, T>;
template <std::size_t N, class T, std::size_t... Is>
auto remove_duplicates_pack_first(T, std::index_sequence<Is...>)
-> std::conditional_t<(... || (N > Is && std::is_same_v<TET<N, T>, TET<Is, T>>)), std::tuple<>, std::tuple<TET<N, T>>>;
template <template <class...> class TT, class... Ts, std::size_t... Is>
auto remove_duplicates(std::tuple<TT<Ts>...> t, std::index_sequence<Is...> is)
-> decltype(std::tuple_cat(remove_duplicates_pack_first<Is>(t, is)...));
template <template <class...> class TT, class... Ts>
auto remove_duplicates(TT<Ts...> t)
-> decltype(unpack<TT>(remove_duplicates<TT>(pack<TT>(t), std::make_index_sequence<sizeof...(Ts)>())));
}
template <class T>
using remove_duplicates_t = decltype(detail::remove_duplicates(std::declval<T>()));
Next, I declare my own custom typelist for using the above code. A pretty straightforward empty struct that most of you have seen before:
template<class...> struct typelist{};
Part 2: our "hash_state"
"hash_state", which I'm calling hash_token
:
template<size_t N, class...Ts>
struct hash_token
{
template<size_t M, class... Us>
constexpr bool operator ==(const hash_token<M, Us...>&)const{return N == M;}
constexpr size_t value() const{return N;}
};
Simply encapsulates a size_t
for the hash value (which you can also access via the value()
function) and a comparator to check if two hash_tokens are identical (because you can have two different type lists but the same hash value. e.g., if you hash int
to get a token and then compare that token to one where you've hashed (int
, float
, char
, int
)).
Part 3: type_hash
function
Finally our type_hash
function:
template<class T, size_t N, class... Ts>
constexpr auto type_hash(T, hash_token<N, Ts...>) noexcept
{
if constexpr(std::is_same_v<remove_duplicates_t<typelist<Ts..., T>>, typelist<Ts...>>)
{
return hash_token<detail::index_of<T, Ts...>(), Ts...>{};
}
else
{
return hash_token<N+1, Ts..., T>{};
}
}
template<class T>
constexpr auto type_hash(T) noexcept
{
return hash_token<0, T>{};
}
The first overload is for the generic case; you've already "hashed" a number of types, and you want to hash yet another one. It checks to see if the type you're hashing has already been hashed, and if so, it returns the index of the type in the unique type list.
To accomplish getting the index of a type in a typelist, I used simple template expansion to save some compile time template instantiations (avoiding a recursive lookup):
// find the first index of T in Ts (assuming T is in Ts)
template<class T, class... Ts>
constexpr size_t index_of()
{
size_t index = 0;
size_t toReturn = 0;
using swallow = size_t[];
(void)swallow{0, (void(std::is_same_v<T, Ts> ? toReturn = index : index), ++index)...};
return toReturn;
}
The second overload of type_hash
is for creating an initial hash_token
starting at 0
.
Usage:
int main()
{
auto x = []{};
auto y = []{};
auto z = x;
std::cout << std::is_same_v<decltype(x), decltype(y)> << std::endl; // 0
std::cout << std::is_same_v<decltype(x), decltype(z)> << std::endl; // 1
constexpr auto xtoken = type_hash(x);
constexpr auto xytoken = type_hash(y, xtoken);
constexpr auto xyztoken = type_hash(z, xytoken);
std::cout << (xtoken == xytoken) << std::endl; // 0
std::cout << (xtoken == xyztoken) << std::endl; // 1
}
Conclusion:
Not really useful in a lot of code, but this may help solve some constrained meta-programming problems.
&typeid(T)
will always be the same for the same type, but this isn't guaranteed. And unfortunatelystd::type_info::hash_code()
is notconstexpr
. – Joestd::type_info::hash_code()
is not constexpr. And I need these hashes at compile-time. – Cahilly
withz
and notx
withz
in second cout, as later there isyhash == zhash
. – Ezraezri1
fromyhash == zhash
? Or maybe fromxhash == zhash
? – Avellanedaconstexpr
– Hertzyhash
be the same aszhash
? Shouldxhash
be the same aszhash
? – Hertz