How to Store Custom Types in a Hash Map

c++11 containers intermediate

std::unordered_map requires that the keys of the map are able to be compared for equality, meaning the keys should support the equality operator (==). The keys of std::unordered_map are also required to be able to be hashable meaning you need to write a specialization of std::hash for the key type. Note that generally we’re not allowed to write code in the std namespace as that’s reserved for the standard library. Hash functions are an exception however and are explicitly allowed for exactly this purpose. Here’s an example:

#include <unordered_map>
#include <string>
#include <iostream>

struct Planet {
  std::string name;

  friend bool operator==(const Planet& a, const Planet& b) {
    return a.name == b.name;
  }
};

// Our custom std::hash specialization for Planet
template <>
struct std::hash<Planet> {
  size_t operator()(const Planet& p) const noexcept {
    return std::hash<std::string>{}(p.name);
  }
};

int main() {
  std::unordered_map<Planet, std::string> descriptions{
    {{"Earth"}, "Home"},
    {{"Mars"}, "The red planet"},
  };

  Planet earth{"Earth"};
  std::cout << descriptions[earth] << "\n"; 
}
Home

Unfortunately C++ does not hold our hand when it comes to hashing. Most of the time objects contain multiple fields and we need to combine individual hashes into a single hash value. We need to implement our own “hash combiner” for this. Here’s an example of a simple hash combiner:

#include <unordered_map>
#include <string>
#include <iostream>

// A simple hash combiner.
// Hashes all args and mixes their hashed bits together 
template <typename ...Args>
size_t hash_all(const Args& ...args) {
  uint64_t hash = 0xc3a5c85c97cb3127ULL; // A large prime number
  // Create a lambda for mixing 64bit hashes
  // https://cppbyexample.com/lambdas.html
  auto hash_mix = [&hash](uint64_t v) {
    hash += v;
    // fmix64 from MurmurHash
    hash ^= hash >> 33;
    hash *= 0xff51afd7ed558ccdULL;
    hash ^= hash >> 33;
    hash *= 0xc4ceb9fe1a85ec53ULL;
    hash ^= hash >> 33;
  };
  // For each arg hash it with std::hash and mix it with hash_mix
  (hash_mix(std::hash<Args>{}(args)), ...);
  return hash;
}

struct Point {
  float x, y, z;

  friend bool operator==(const Point& a, const Point& b) {
    return a.x == b.x && a.y == b.y && a.z == b.z;
  }
};

// Our custom std::hash specialization for Point
template <>
struct std::hash<Point> {
  size_t operator()(const Point& p) const noexcept {
    return hash_all(p.x, p.y, p.z);
  }
};

int main() {
  std::unordered_map<Point, std::string> descriptions{
    {{0, 0, 0}, "The Origin"},
    {{1, 2, 3}, "Point 123"},
    {{4, 5, 6}, "Point 456"},
  };

  std::cout << descriptions[Point{0, 0, 0}] << "\n"; 
  std::cout << descriptions[Point{4, 5, 6}] << "\n"; 
}
The Origin
Point 456

It may come as no surprise after the above example that hashing objects is not exactly a trivial topic. Using std::hash and the combiner above is fine for std::unordered_map but they alone aren’t enough to produce cryptographic hashes for passwords or privacy purposes. Truly strong hashes take much more code than can easily fit in a simple example like this. A great resource to learn about other C++ hash functions, and their implementations, is the SMHasher Github project. For our purposes here, just know that the provided hash_all function is sufficient for non-cryptographic hashes such as the ones used for std::unordered_map.


For more C++ By Example, click here.