Making Strong Types Hashable
Strong types are types that are built over primitive types, and add meaning to them. My purpose today is two-fold:
- showing you how to write an STL-compliant hash function for custom types so that they can be used in unordered containers such as
std::unordered_map
,
- making a hash function available for strong types.
For more about the motivation and implementation of strong types, I suggest you read Strong types for strong interfaces first, as we’ll use the NamedType
class and in particular its feature to inherit functionalities from the underlying type.
Strong types are an essential tool to bring expressiveness into code. Here is the series dedicated to strong types on Fluent C++:
- Strongly typed constructors
- Strong types for strong interfaces
- Passing strong types by reference
- Strong lambdas: strong typing over generic types
- Good news: strong types are (mostly) free in C++
- Inheriting functionalities from the underlying type
- Making strong types hashable
- Converting strong units to one another
- Metaclasses, the Ultimate Answer to Strong Typing in C++?
- Making strong types implicitly convertible
Implementing a hash function in C++
Since C++11, the standard offers an std::hash
structure declared in the namespace std
:
namespace std { template< class Key > struct hash; }
The standard also specifies specializations for this structure for a fair amount of standard types. There are about 30 such types, including int
, bool
, char
, double
, std::string
, and even some generic types such as T*
, std::optional<T>
or std::unique_ptr<T>
, with a fallback on the template type hashing in the latter case.
These specializations of std::hash
have notably 2 methods:
- a default constructor (taking no parameter),
- an
operator()
, whose prototype is of the formsize_t operator()(Key const&) const;
and which actually does the job of providing a hash value (of type
size_t
) from an object of the type insidestd::hash
.
On the other side, the unordered containers of the STL such as std::unordered_map
accept a hash structure in their template parameters. And this template has a default value of std::hash
specialized on the Key type of the container:
template< class Key, class T, class Hash = std::hash<Key>, class KeyEqual = std::equal_to<Key>, class Allocator = std::allocator< std::pair<const Key, T> > > class unordered_map;
The container builds hash objects of type Hash
, and invokes them on an element whenever it needs a hash value, like when inserting or searching a key in the container.
Making strong types hashable
Our purpose will be here to allow any strong type to inherit from the hash function of its underlying type, if it exists. And this functionality should be explicitly asked for when defining the strong type, exactly like the other functionalities inherited from the underlying type.
To illustrate, let’s take the example of a type representing a serial number, modeled by a string. We want to be able to define the serial number as a strong type like so:
using SerialNumber = NamedType<std::string, SerialNumberTag, Comparable, Hashable>;
(Comparable
provides operator==
inherited from the underlying type, also used by the STL hash table via std::equal_to
visible in the above definition of std::unordered_map
).
So let’s specialize std::hash
for our NamedType
class:
namespace std { template <typename T, typename Parameter, typename Converter, template<typename> class... Skills> struct hash<NamedTypeImpl<T, Parameter, Converter, Skills...>> { size_t operator()(NamedTypeImpl<T, Parameter, Converter, Skills...> const& x) const { return std::hash<T>()(x.get()); } }; }
Despite its bushy aspect the above code is really easy to understand. The class that we progressively built along the posts of this series to represent strong types is:
template <typename T, typename Parameter, typename Converter, template<typename> class... Skills> class NamedTypeImpl<T, Parameter, Converter, Skills...>;
and the rest is just putting into std::hash
and calling std::hash
on the underlying type.
Are we done then?
Almost, but not quite. With the above implementation, every strong type will be hashable. However, we want this feature to be activated on demand, by including Hashable
in the list of skills to be inherited from the underlying type. And the feature is not asked for explicitly, we’d like the above code of the specialization to go away.
Said differently, we want this code to be enabled if the strong type is Hashable. This sounds like a job for std::enable_if
.
The class representing strong types inherits from its policies such as Hashable
and Comparable
. So let’s define Hashable
simply as a token:
template<typename T> struct Hashable { static constexpr bool is_hashable = true; };
And base the enabling of the specialization of std::hash
on the presence of this token. Look at the using
declarations added to the below specialization, that rely on enable_if
to make the instantiation of the structure valid or not:
namespace std { template <typename T, typename Parameter, typename Converter, template<typename> class... Skills> struct hash<NamedTypeImpl<T, Parameter, Converter, Skills...>> { using NamedType = NamedTypeImpl<T, Parameter, Converter, Skills...>; using checkIfHashable = typename std::enable_if<NamedType::is_hashable, void>::type; size_t operator()(NamedTypeImpl<T, Parameter, Converter, Skills...> const& x) const { return std::hash<T>()(x.get()); } }; }
And this does the job. The following code:
using SerialNumber = NamedType<std::string, struct SerialNumberTag, Comparable, Hashable>; std::unordered_map<SerialNumber, int> hashMap = { {SerialNumber{"AA11"}, 10}, {SerialNumber{"BB22"}, 20} }; std::cout << hashMap[SerialNumber{"BB22"}] << '\n';
outputs 20.
And the same code without Hashable
in the strong type declaration yields a compile error.
If you want to see the code, have a look at the GitHub repository for NamedType.
Related articles:
- Strongly typed constructors
- Strong types for strong interfaces
- Passing strong types by reference
- Strong lambdas: strong typing over generic types
- Good news: strong types are (mostly) free in C++
- Inheriting functionalities from the underlying type
- Converting strong units to one another
- Metaclasses, the Ultimate Answer to Strong Typing in C++?
- Making strong types implicitly convertible
Share this post!