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.
Hash Combining Deep Dive
The hash_combine formula shown in the examples is the de-facto standard in C++ for mixing hash values:
seed ^= v + 0x9e3779b97f4a7c15ULL + (seed << 6) + (seed >> 2);Each term serves a purpose:
seed ^ vβ incorporates the new value.+ 0x9e3779b97f4a7c15ULLβ the 64-bit form of the golden-ratio constant (2βΆβ΄/Ο β0x9e3779b97f4a7c15). It breaks the symmetry that pure XOR has and ensures even a zero-valuedvperturbs the seed.+ (seed << 6) + (seed >> 2)β two cheap bit-rotation-like shifts that feed previous seed state back into the mix, creating avalanche across word-width bits.
Why XOR alone fails. Plain h1 ^ h2 has two fatal properties for a combiner:
- Symmetric β
h(a, b) == h(b, a). Swapping two fields produces the same combined hash. - Self-cancelling β
x ^ x == 0. A struct with two equal fields always hashes to the seed for those bits.
Both properties concentrate many distinct keys onto the same hash, causing O(n) degradation in unordered containers.
C++26 standardisation. std::hash_combine and std::hash_value are being standardised in C++26 (proposal P2823). Until compiler support arrives, the golden-ratio pattern is the portable alternative used by Boost, Abseil, and most major C++ libraries.
FNV-1a: a portable byte-level combiner
For non-trivial byte-sequence types (network packets, byte buffers, custom string encodings) where you cannot delegate to std::hash<std::string_view>, FNV-1a is a well-tested, zero-dependency option:
// FNV-1a 64-bit β combines individual bytes rather than pre-hashed values
constexpr std::size_t fnv1a(const std::byte* data, std::size_t len) noexcept {
std::size_t h = 14695981039346656037ULL; // FNV offset basis
for (std::size_t i = 0; i < len; ++i) {
h ^= static_cast<std::size_t>(data[i]);
h *= 1099511628211ULL; // FNV prime
}
return h;
}FNV-1a is not a cryptographic hash but has good avalanche properties, is branch-free, and compiles to a tight loop with no external dependencies. For bulk data at very high throughput, wyhash is a faster modern alternative that also passes the SMHasher test suite.
Transparent/Heterogeneous Lookup: the is_transparent Contract
C++20 added heterogeneous overloads of find, count, contains, and equal_range to unordered containers (ordered containers gained them earlier in C++14). The feature activates only when both the Hash and the KeyEqual policy types expose a nested type named is_transparent:
struct StringHash {
using is_transparent = void; // any type works; just has to exist
// Accept anything convertible to string_view β one overload covers all
std::size_t operator()(std::string_view sv) const noexcept {
return std::hash<std::string_view>{}(sv);
}
};
struct StringEqual {
using is_transparent = void;
bool operator()(std::string_view a, std::string_view b) const noexcept {
return a == b;
}
};
std::unordered_map<std::string, Config, StringHash, StringEqual> registry;
registry.emplace("timeout", cfg);
// C++20: all three avoid constructing a temporary std::string
std::string_view key = "timeout";
registry.find(key); // no heap allocation
registry.contains(key); // no heap allocation
registry.count(key); // no heap allocationWhy both Hash and KeyEqual must opt in. The container's lookup algorithm must guarantee that if KeyEqual{}(lookup_key, stored_key) is true, then Hash{}(lookup_key) == Hash{}(stored_key) is also true. Without that invariant, the table would search the wrong bucket. Requiring is_transparent on both forces the programmer to consciously provide a consistent pair.
The heterogeneous hash requirement. The hash functor must return the same value for a std::string_view probe as it would for the std::string key that was stored. Delegating to std::hash<std::string_view>{} satisfies this because the standard guarantees:
// Guaranteed by the standard (cppreference Notes, C++17):
// std::hash<std::string> and std::hash<std::string_view> produce
// identical results for the same character sequence.
assert(std::hash<std::string>{}("hello") ==
std::hash<std::string_view>{}("hello"));Prior art in ordered containers. std::map and std::set use std::less<void> (C++14) as the transparent comparator. For ordered containers only Compare needs is_transparent; there is no hash involved. The unordered extension requires both policy types.
Hash Quality and the Avalanche Effect
A hash function exhibits the strict avalanche criterion when flipping any single input bit changes each output bit with 50% probability. High-quality general-purpose hashes (wyhash, xxHash, MurmurHash3) are engineered to approach this. The standard makes no such guarantee:
"The actual hash functions are implementation-dependent and are not required to fulfill any other quality criteria except those specified above. Notably, some implementations use trivial (identity) hash functions which map an integer to itself." β cppreference Notes on
std::hash
Why identity hashes fail in practice. Many hash table implementations (including some versions of libstdc++) choose a power-of-two bucket count and compute bucket = hash_value & (bucket_count - 1). An identity hash for integers places all keys {0, 16, 32, 48, ...} in the same bucket, producing O(n) lookup even with a correct hash function contract.
Applying one mixing step prevents this:
// Fibonnaci hashing / multiplicative hashing for integers
// Multiplying by 2654435761 (Knuth's multiplicative constant) and
// shifting mixes the low bits up into the high bits.
struct IntHash {
std::size_t operator()(std::uint32_t x) const noexcept {
x = ((x >> 16) ^ x) * 0x45d9f3bU;
x = ((x >> 16) ^ x) * 0x45d9f3bU;
x = (x >> 16) ^ x;
return x;
}
};
std::unordered_set<std::uint32_t, IntHash> ids;Collision-based DoS (hash flooding). When keys come from untrusted input (HTTP headers, query parameters, user-supplied JSON keys), an attacker who knows the hash function can craft inputs that all collide into one bucket. Implementations defend against this by seeding the hash with a per-process random value at startup β which is why std::hash values are not stable across runs.
Quality alternatives for non-standard use. When you need a hash outside the std::unordered_* ecosystem (e.g., building your own table or a probabilistic data structure), prefer:
- FNV-1a β simple, no external dependency, zero-allocation, good avalanche.
- wyhash β state-of-the-art speed and quality, single-header, passes SMHasher.
- xxHash β excellent throughput for bulk data, widely audited.
None of these should be used as cryptographic hashes.
Hash Portability and Stability Guarantees
The C++ standard gives std::hash very narrow stability guarantees. Misunderstanding this causes subtle, hard-to-reproduce bugs.
What is guaranteed:
- Within a single run of one program, equal values produce equal hashes.
- The call operator is
noexcept(for standard-library specialisations).
What is NOT guaranteed:
- The same value may produce a different hash in the next invocation of the same binary.
- Two different implementations (GCC vs MSVC, libstdc++ vs libc++) will produce different hashes for the same input.
- Even two compilations with the same compiler may differ if ASLR seeds the hash differently.
This is intentional. libstdc++ has randomised std::hash<std::string> by default since GCC 6 to resist hash-flooding attacks. libc++ does the same.
Practical consequences:
// WRONG: persisting hash values across runs
void save_index(const std::string& key, int value) {
std::size_t h = std::hash<std::string>{}(key);
db.store(h, value); // h will be different next run β lookups will fail
}
// WRONG: using std::hash in a distributed system
// Two machines will produce different hashes; consistent hashing breaks.
// CORRECT: use a fixed-output, cross-platform hash for persistence
// Options: CRC-32C, FNV-1a with a fixed seed, xxHash with seed=0
constexpr std::uint64_t stable_hash(std::string_view s) noexcept {
// FNV-1a with the standard offset basis and prime (never changes)
std::uint64_t h = 14695981039346656037ULL;
for (unsigned char c : s) {
h ^= c;
h *= 1099511628211ULL;
}
return h;
}Rule of thumb: use std::hash for in-process data structures; use a fixed-seed, documented hash algorithm for anything that crosses process, machine, or time boundaries (serialisation, databases, caches, network protocols).
Hash Combining: Choosing Between hash_combine and FNV-1a
Two patterns dominate portable C++ hash combining, and picking the wrong one for the context causes subtle quality problems. The golden-ratio hash_combine operates on pre-hashed size_t values and is ideal when each field already has a good std::hash specialization. FNV-1a operates on raw bytes and is better when you control a contiguous memory region or are building a custom string encoding from scratch.
// C++17
// hash_combine: mix already-hashed field values
// Best for structs with heterogeneous field types that each have std::hash
inline void hash_combine(std::size_t& seed, std::size_t v) noexcept {
seed ^= v + 0x9e3779b97f4a7c15ULL + (seed << 6) + (seed >> 2);
}
// FNV-1a: hash a raw byte range
// Best for flat byte buffers, fixed-width POD structs, custom encodings
constexpr std::size_t fnv1a_bytes(const void* data, std::size_t len) noexcept {
const auto* p = static_cast<const unsigned char*>(data);
std::size_t h = 14695981039346656037ULL; // 64-bit FNV offset basis
for (std::size_t i = 0; i < len; ++i) {
h ^= p[i];
h *= 1099511628211ULL; // 64-bit FNV prime
}
return h;
}Plain XOR (h1 ^ h2) fails in two distinct ways that both degrade hash table performance to O(n). First, XOR is commutative, so swapping any two fields of a struct produces an identical combined hash β structs {1, 2} and {2, 1} collide. Second, XOR is self-inverse, so any struct with two equal fields ({x, x}) always contributes zero for those bits regardless of x's value.
// C++17
// Demonstrating why XOR is wrong as a combiner
struct Bad {
std::size_t operator()(std::pair<int,int> p) const noexcept {
// WRONG: symmetric and self-cancelling
return std::hash<int>{}(p.first) ^ std::hash<int>{}(p.second);
}
// hash({3,5}) == hash({5,3}) β symmetric collision
// hash({7,7}) == 0 β self-cancellation
};
struct Good {
std::size_t operator()(std::pair<int,int> p) const noexcept {
// C++17: correct asymmetric combiner
std::size_t seed = 0;
hash_combine(seed, std::hash<int>{}(p.first));
hash_combine(seed, std::hash<int>{}(p.second));
return seed;
}
};The golden-ratio constant 0x9e3779b97f4a7c15 is the 64-bit Fibonacci/golden-ratio hashing constant (2βΆβ΄ Γ· Ο). Adding it to the shifted seed means even a zero-valued new hash perturbs the running state, breaking the self-cancellation that plagues XOR-only combiners.
std::hash Standard-Library Specializations by C++ Version
The set of types with built-in std::hash specializations has grown substantially across C++11, C++17, and C++20, and using a type without a specialization is a hard compile-time error. Knowing which types are covered avoids writing unnecessary specializations and prevents accidental use of the undefined primary template.
// C++11
// All of the following have std::hash specializations in C++11:
// bool, char, signed char, unsigned char, char16_t, char32_t, wchar_t
// short, unsigned short, int, unsigned int, long, unsigned long
// long long, unsigned long long, float, double, long double
// T* (all pointer types β hashes the address, not the pointed-to value)
// std::string, std::wstring, std::u16string, std::u32string
// std::bitset<N>, std::vector<bool>
// std::thread::id, std::type_index
// std::error_code, std::error_condition
#include <functional>
#include <string>
#include <thread>
#include <typeindex>
#include <system_error>
std::hash<std::string>{}("hello"); // OK β C++11
std::hash<std::thread::id>{}(std::this_thread::get_id()); // OK β C++11C++17 expanded coverage to include string-view types and the vocabulary types introduced in that standard. std::hash<std::optional<T>> is defined only when std::hash<T> is defined; an optional holding a value hashes to the hash of that value, while an empty optional hashes to an implementation-defined value distinct from any non-empty result.
// C++17
// New specializations added in C++17:
// std::string_view, std::wstring_view, std::u16string_view, std::u32string_view
// std::optional<T> β defined iff std::hash<T> is defined
// std::variant<Ts...> β defined iff std::hash<Ti> is defined for every Ti
// std::monostate
// std::filesystem::path
#include <optional>
#include <variant>
#include <filesystem>
#include <string_view>
std::hash<std::string_view>{}("hello"); // C++17 β no allocation
std::hash<std::optional<int>>{}(std::nullopt); // C++17 β defined
std::hash<std::optional<int>>{}(42); // C++17 β same as hash<int>{}(42)
std::hash<std::filesystem::path>{}("/usr/bin"); // C++17
using V = std::variant<int, std::string>;
std::hash<V>{}(V{42}); // C++17 β hashes the active alternativeC++20 adds char8_t and the corresponding std::u8string and std::u8string_view types. Notably, there is still no std::hash specialization for std::pair or std::tuple in any standard version β these remain the most common types programmers must specialize manually.
// C++20
// Added in C++20:
// char8_t, std::u8string, std::u8string_view
// std::pmr::string and other pmr:: string aliases
// std::coroutine_handle<P> β hashes the frame pointer
#include <coroutine>
std::hash<char8_t>{}(u8'A'); // C++20
// Still NOT provided by any standard version (must be user-defined):
// std::pair<K,V>, std::tuple<Ts...>, std::array<T,N>, std::complex<T>Transparent Hashing with std::string_view Interop
Heterogeneous lookup eliminates temporary std::string allocations that otherwise occur on every find, count, or contains call when the probe key is a const char* or std::string_view. Before C++20 each of those lookups constructed a heap-allocated std::string just to compare against stored keys; with transparent hashing the probe value is used directly.
The feature depends on a guarantee the standard makes explicit: std::hash<std::string> and std::hash<std::string_view> must return the same value for the same character sequence. This cross-type stability makes it safe to store std::string keys while probing with std::string_view.
// C++20
#include <unordered_map>
#include <string>
#include <string_view>
#include <cassert>
// Guaranteed by the standard β same character sequence, same hash:
assert(std::hash<std::string>{}("lookup") ==
std::hash<std::string_view>{}("lookup")); // always true
struct TransparentHash {
using is_transparent = void; // C++20: enable heterogeneous overloads
std::size_t operator()(std::string_view sv) const noexcept {
// string_view overload covers std::string, const char*, string_view
return std::hash<std::string_view>{}(sv);
}
};
struct TransparentEqual {
using is_transparent = void; // C++20: must also opt in
bool operator()(std::string_view a, std::string_view b) const noexcept {
return a == b;
}
};
std::unordered_map<std::string, int, TransparentHash, TransparentEqual> env;
env.emplace("HOME", 1);
// All of the following probe without constructing a temporary std::string:
std::string_view sv = "HOME";
const char* raw = "HOME";
env.find(sv); // C++20 β no allocation
env.find(raw); // C++20 β no allocation
env.contains(sv); // C++20 β no allocation
env.count(sv); // C++20 β no allocationIf is_transparent is missing from either the hash or the equality functor, the heterogeneous overloads do not exist and the call silently falls back to constructing a temporary key. There is no compiler warning; the code compiles and works correctly but wastes an allocation on every probe. The only way to verify the optimization is active is to inspect the overload set or measure allocations with a custom allocator.
// C++20
// Verifying heterogeneous overloads are reachable at compile time:
static_assert(
std::is_invocable_v<
decltype(&decltype(env)::template find<std::string_view>),
decltype(env)&,
std::string_view>,
"heterogeneous find not available β check is_transparent on Hash and Equal"
);For ordered containers (std::map, std::set) the same is_transparent mechanism has existed since C++14 via std::less<void>, but only KeyCompare needs it β there is no hash involved. The unordered extension in C++20 adds the same marker requirement to both Hash and KeyEqual to enforce the invariant that lookup keys and stored keys hash to the same bucket.
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)