Skip to content
C++
Library
since C++11
Basic

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++11

A 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:

  1. 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.
  2. Equality consistency β€” if a == b, then std::hash<T>{}(a) == std::hash<T>{}(b).
  3. 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

cpp
#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:

cpp
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

cpp
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:

cpp
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 β€” found

Transparent 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:

cpp
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++20

Prior 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:

cpp
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 ^ h2 is 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 for n elements and avoids incremental rehashing.
  • Match Hash and KeyEqual semantics. Every pair of objects equal under KeyEqual must 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_view overloads. A single operator()(std::string_view) handles const char*, std::string, and std::string_view without 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:

cpp
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 true

Relying 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 of std::hash
  • std::equal_to<void> β€” transparent equality for ordered containers (C++14); its counterpart for unordered containers requires is_transparent on both Hash and KeyEqual (C++20)
  • std::ranges::equal_to β€” constrained equality callable (C++20)