Skip to Content
Course content

67: Unordered Containers and Hashing

Click on the "Edit" button in the top corner of the screen to edit your slide content.

You’ve likely spent a fair amount of time with std::map and std::set. They're reliable, and they keep everything sorted. But eventually, you'll hit a performance wall where the logarithmic lookup time (O(log n)) isn't fast enough, or you simply don't care about the order of your elements. That's where unordered containers come in.

However, switching from a map to an unordered_map isn't always a "drop-in" replacement. I see this mistake all the time when developers start using custom objects as keys. Take a look at this snippet:

struct Point {
    int x, y;
    bool operator==(const Point& other) const {
        return x == other.x && y == other.y;
    }
};

// ... inside some function ...
std::unordered_map<Point, std::string> grid_labels;
grid_labels[{10, 20}] = "Start Node"; // ERROR: Compilation fails here

The Case of the Missing Hash Specialization

If you try to compile the code above, your compiler will throw a wall of text at you. It's intimidating, but if you squint through the noise, the core issue is that std::unordered_map doesn't know how to handle a Point.

Unlike std::map, which only requires a "less than" operator (<) to build a balanced tree, std::unordered_map uses a hash table. It needs two things to function: a way to check if two keys are equal (which we provided with operator==) and a hash function that turns your object into a size_t integer. C++ provides hash functions for primitives like int and std::string, but it has no idea how to hash your custom Point struct.

Teaching the Map How to Hash Your Custom Types

To fix this, we need to provide a specialization for std::hash. I prefer doing this by injecting a specialization into the std namespace. The trick is to combine the hashes of the individual members of your struct.

#include <unordered_map>
#include <functional> // Required for std::hash

struct Point {
    int x, y;
    bool operator==(const Point& other) const {
        return x == other.x && y == other.y;
    }
};

// We tell std::hash how to handle Point
namespace std {
    template <typename T>
    struct hash<Point> {
        size_t operator()(const Point& p) const {
            // A simple way to combine hashes:
            // We hash x and y separately, then XOR them with a shift
            // to prevent (1, 2) and (2, 1) from having the same hash.
            return hash<int>{}(p.x) ^ (hash<int>{}(p.y) << 1);
        }
    };
}

// Now this works perfectly
std::unordered_map<Point, std::string> grid_labels;
grid_labels[{10, 20}] = "Start Node";

The ^ (hash << 1) part is a basic way to "mix" the bits. In a production environment, you might use a more robust combining function (like boost::hash_combine), but for most tasks, this suffices. By doing this, you've moved your lookup time from O(log n) to O(1) on average.

Choosing Between Ordered and Unordered

I often get asked: "Why not just use unordered_map for everything since it's faster?" It's a tempting thought, but there are trade-offs. Unordered containers use more memory to maintain the bucket system. More importantly, they offer no guarantees about the order of elements. If you need to iterate through your keys in alphabetical or numerical order, you're stuck with std::map.

Also, keep in mind that while the average case for unordered_map is O(1), the worst case is actually O(n) if many different keys happen to hash to the same bucket (a "collision"). It's rare, but in high-security systems, "hash flooding" attacks can be used to intentionally cause these collisions and slow your program to a crawl. If you're building something where deterministic performance is more important than raw speed, stick to the ordered map.




📋 Practical Task

Build a Coordinate-Based Game Entity Tracker

You are building a simple 2D game engine. You need to track which "Entity" (like a monster, a treasure chest, or a NPC) occupies which coordinate on a grid. Since the game world is huge, you cannot use a 2D array; you must use an unordered container to store only the coordinates that actually contain something.

Your Task:

  • Create a struct Coordinate with int x and int y.
  • Implement the necessary equality operator and std::hash specialization to allow Coordinate to be used as a key in a std::unordered_map.
  • Create a std::unordered_map<Coordinate, std::string> called world_map.
  • Insert at least three different entities at different coordinates (e.g., {5, 10}: "Goblin", {12, 2}: "Health Potion").
  • Write a function std::string check_location(int x, int y) that looks up a coordinate in the map and returns the entity name if it exists, or "Empty Space" if it doesn't.

Constraint: Your solution must compile without errors and successfully distinguish between {1, 2} and {2, 1}.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.