std::hash and Custom Hash Functions
std::hash specialization, hash_combine design, transparent heterogeneous lookup (C++20), and custom hash requirements for unordered containers.
std::hashsince C++11A function-object template in <functional> that maps a value of type T to a size_t hash code; the standard provides specializations for built-in and common library types, and you must add your own for any custom type used as a key in an unordered associative container.
Overview
std::hash<T> must satisfy three requirements:
- Determinism β equal objects produce equal hash codes within a single program execution. Across executions, values may differ; implementations are permitted (and common) to seed with a runtime random value to resist hash-flooding attacks.
- Equality consistency β if
a == b, thenstd::hash<T>{}(a) == std::hash<T>{}(b). - noexcept β the call operator must not propagate exceptions.
A fourth property β uniform distribution β is not mandated but governs practical O(1) performance. A hash returning a constant satisfies the standard while degrading every unordered container to O(n) lookup.
Built-in specializations (C++11): all arithmetic types, all pointer types, std::string, std::wstring, std::u16string, std::u32string, std::bitset, std::vector<bool>, std::thread::id, std::type_index, std::error_code, std::error_condition.
C++17 additions: std::string_view and wide/Unicode view variants, std::optional<T>, std::variant, std::monostate, std::filesystem::path.
The primary template for any unlisted type has no definition β invoking it is ill-formed.
Syntax
#include <functional>
// Primary template β undefined for types without a specialization
template<typename T>
struct std::hash;
// Specialization (C++11)
template<>
struct std::hash<MyType> {
std::size_t operator()(const MyType& v) const noexcept;
};
// Unordered container signature β showing all four policy slots
template<
class Key,
class Hash = std::hash<Key>, // must satisfy Hash named requirement
class KeyEqual = std::equal_to<Key>, // must agree with Hash
class Alloc = std::allocator<Key>
> class unordered_set;Examples
hash_combine and a variadic helper
Never combine field hashes with plain XOR: it is symmetric (hash(a,b) == hash(b,a)) and self-cancelling (x ^ x == 0). The constant below is 2βΆβ΄/Ο (derived from the golden ratio), which spreads bits uniformly across the word width:
inline void hash_combine(std::size_t& seed, std::size_t v) noexcept {
seed ^= v + 0x9e3779b97f4a7c15ULL + (seed << 6) + (seed >> 2);
}
// Fold an arbitrary number of hashable values β C++17 fold expression
template<typename... Ts>
std::size_t hash_values(const Ts&... vals) noexcept {
std::size_t seed = 0;
(hash_combine(seed, std::hash<Ts>{}(vals)), ...); // C++17
return seed;
}Specializing for a struct
struct ServiceKey {
std::string host;
std::uint16_t port;
bool tls;
};
bool operator==(const ServiceKey& a, const ServiceKey& b) noexcept {
return a.host == b.host && a.port == b.port && a.tls == b.tls;
}
template<>
struct std::hash<ServiceKey> {
std::size_t operator()(const ServiceKey& k) const noexcept {
return hash_values(k.host, k.port, k.tls);
}
};
std::unordered_map<ServiceKey, ConnectionPool> pools;
pools[{"api.internal", 443, true}].acquire();Case-insensitive hash with matching equality
When customising the hash, KeyEqual must agree β objects considered equal by the comparator must produce identical hashes:
struct CiHash {
std::size_t operator()(std::string_view s) const noexcept { // C++17
std::size_t seed = 0;
for (unsigned char c : s)
hash_combine(seed, std::size_t(std::tolower(c)));
return seed;
}
};
struct CiEqual {
bool operator()(std::string_view a, std::string_view b) const noexcept {
return std::equal(a.begin(), a.end(), b.begin(), b.end(),
[](unsigned char c1, unsigned char c2) {
return std::tolower(c1) == std::tolower(c2);
});
}
};
std::unordered_map<std::string, std::string, CiHash, CiEqual> headers;
headers["Content-Type"] = "application/json";
headers.count("content-type"); // 1 β foundTransparent heterogeneous lookup (C++20)
C++20 adds overloads of find, count, contains, and equal_range that accept a compatible key type without constructing the stored key type. Both Hash and KeyEqual must declare the is_transparent marker:
struct StringHash {
using is_transparent = void; // C++20: opt-in to heterogeneous lookup
std::size_t operator()(std::string_view sv) const noexcept {
return std::hash<std::string_view>{}(sv);
}
// std::string and const char* implicitly convert to string_view above
};
struct StringEqual {
using is_transparent = void; // C++20
bool operator()(std::string_view a, std::string_view b) const noexcept {
return a == b;
}
};
std::unordered_map<std::string, int, StringHash, StringEqual> config;
config["timeout"] = 30;
// All of the following avoid constructing a temporary std::string (C++20):
std::string_view sv = "timeout";
const char* raw = "timeout";
config.find(sv); // no allocation
config.find(raw); // no allocation
config.contains(sv); // C++20Prior to C++20, heterogeneous lookup existed only for ordered containers via std::less<void>.
Hashing enum types
No standard specialization exists for enum types. Delegate to the underlying integer type:
enum class Permission : std::uint8_t { Read = 1, Write = 2, Exec = 4 };
template<>
struct std::hash<Permission> {
std::size_t operator()(Permission p) const noexcept {
return std::hash<std::uint8_t>{}(
static_cast<std::uint8_t>(p));
}
};
std::unordered_set<Permission> granted;
granted.insert(Permission::Read);Best Practices
- Use
hash_combine, not XOR.h1 ^ h2is symmetric and produces 0 for equal inputs. - Mark
operator()noexcept. The standard requires it; omitting it may cause compilation failures in some contexts and misleads callers. - Reserve before bulk inserts.
map.reserve(n)preallocates fornelements and avoids incremental rehashing. - Match
HashandKeyEqualsemantics. Every pair of objects equal underKeyEqualmust produce the same hash. This constraint is the programmer's responsibility β the container does not validate it. - Do not mutate keys after insertion. Keys in unordered containers are effectively
const. Mutation silently corrupts bucket placement without any error. - Centralise on
string_viewoverloads. A singleoperator()(std::string_view)handlesconst char*,std::string, andstd::string_viewwithout duplicating overloads.
Common Pitfalls
std::hash<const char*> hashes the pointer, not the string content. Two pointers to the same text are different values unless the compiler merges them, which is not guaranteed:
std::hash<const char*>{}("hello") == std::hash<const char*>{}("hello");
// may be false β hashing addresses, not characters
std::hash<std::string_view>{}("hello") == std::hash<std::string_view>{}("hello");
// guaranteed trueRelying on cross-run hash stability. The standard permits implementations to randomise the seed per execution to resist algorithmic complexity attacks. Do not persist, serialize, or compare hash values across runs. libstdc++ randomises std::hash<std::string> by default since GCC 6.
Hashing NaN floating-point values. IEEE 754 defines NaN != NaN, which breaks the equality-consistency requirement for any type that can represent NaN. Normalize NaN to a canonical bit pattern before hashing if your type permits it.
Forgetting KeyEqual when using a custom Hash. Supplying only the Hash template argument leaves KeyEqual as std::equal_to<Key>. If your hash treats distinct keys as equivalent (e.g., case-insensitive), the default equality will allow duplicates the hash intended to merge.
Identity hash with power-of-two bucket counts. Many implementations pick a power-of-two bucket count and reduce via bitmasking. An identity hash (return x) places all keys with the same low bits in the same bucket. Apply at least one mixing step even for integer keys.
See Also
std::unordered_map,std::unordered_set,std::unordered_multimap,std::unordered_multisetβ consumers ofstd::hashstd::equal_to<void>β transparent equality for ordered containers (C++14); its counterpart for unordered containers requiresis_transparenton bothHashandKeyEqual(C++20)std::ranges::equal_toβ constrained equality callable (C++20)