How to Use std::hash for Custom Types in C++
This article explains how the C++ standard library's
std::hash template generates hash keys for custom
user-defined data structures. Because C++ lacks built-in reflection,
std::hash cannot automatically inspect or hash user-defined
classes and structs. To use custom types inside unordered associative
containers such as std::unordered_map or
std::unordered_set, developers must explicitly define how
to hash their types by specializing std::hash or supplying
a custom hash functor, while also satisfying the equality contract.
Why std::hash
Needs Custom Definitions
The standard library provides template specializations of
std::hash<T> for fundamental types (such as
int, double, and pointers) and select standard
library types (like std::string and
std::vector<bool>). When a user-defined struct or
class is passed as a key to an unordered container, the compiler checks
for a matching std::hash specialization. If none exists,
compilation fails.
To compute a hash for a custom structure, the developer must combine the hashes of the individual member variables that define the object's identity.
Implementing a Hash Function
There are two primary methods to enable hashing for a custom type:
specializing the std::hash template in the std
namespace, or creating a standalone functor.
1. Template Specialization
of std::hash
Specializing std::hash<T> allows your custom type
to be used by default without explicitly specifying a hash functor in
container declarations.
#include <iostream>
#include <string>
#include <unordered_set>
struct Employee {
int id;
std::string name;
// operator== is required to resolve hash collisions
bool operator==(const Employee& other) const {
return id == other.id && name == other.name;
}
};
// Inject the specialization into namespace std
namespace std {
template <>
struct hash<Employee> {
std::size_t operator()(const Employee& e) const noexcept {
std::size_t h1 = std::hash<int>{}(e.id);
std::size_t h2 = std::hash<std::string>{}(e.name);
// Combine hashes using bit-mixing
return h1 ^ (h2 + 0x9e3779b9 + (h1 << 6) + (h1 >> 2));
}
};
}2. Passing a Custom Functor
Alternatively, a custom functor can be defined and passed directly as
a template argument to containers like std::unordered_map
or std::unordered_set. This keeps the std
namespace untouched:
struct EmployeeHasher {
std::size_t operator()(const Employee& e) const noexcept {
std::size_t h1 = std::hash<int>{}(e.id);
std::size_t h2 = std::hash<std::string>{}(e.name);
return h1 ^ (h2 + 0x9e3779b9 + (h1 << 6) + (h1 >> 2));
}
};
// Usage:
std::unordered_set<Employee, EmployeeHasher> employee_set;How Hashes Are Combined
A primary challenge when hashing custom structures is combining the
discrete hash values of multiple fields into a single
std::size_t output.
Directly XORing (^) individual hashes is often
insufficient because identical values cancel each other out or create
order-independent hashes (e.g., hashing a pair (A, B)
yields the same value as (B, A)). A widely adopted pattern
is the hash combination formula used in Boost
(boost::hash_combine):
\[\text{seed} \leftarrow \text{seed} \oplus (\text{hash}(v) + \text{0x9e3779b9} + (\text{seed} \ll 6) + (\text{seed} \gg 2))\]
The constant 0x9e3779b9 is the fractional part of the
golden ratio (\(2^{32} / \phi\))
represented in 32-bit hexadecimal. Adding this constant and bit-shifting
ensures that changes in any input bit propagate thoroughly across all
bits of the combined result.
Essential Requirements for Custom Hashes
To function correctly within C++ associative containers, a hash implementation must uphold three rules:
- Determinism: For any given object, the hash function must always produce the same value throughout its lifecycle in the container. Modifying a key while it resides in an associative container leads to undefined behavior.
- Equality Consistency: If two objects are equal
according to
operator==, their hash values must be identical: \[a == b \implies \text{hash}(a) == \text{hash}(b)\] If two objects have identical hashes, they are not strictly required to be equal (a hash collision), but collisions should be minimized. - Uniform Distribution: The function should
distribute generated keys as evenly as possible across the range of
std::size_tto maintain the average \(O(1)\) lookup performance of hash buckets.