Какие требования предъявляются к классу, чтобы его можно было использовать в качестве ключа в ассоциативных контейнерах C++?

Ответ

Требования зависят от типа контейнера:

1. Для упорядоченных контейнеров (std::map, std::set, std::multimap, std::multiset): Ключ должен поддерживать строгое слабое упорядочение. На практике это означает, что для типа ключа должен быть определен оператор сравнения operator< или предоставлен пользовательский функтор-компаратор.

// Пример ключа для std::set или std::map
struct Point {
    int x, y;
    // Определяем оператор < для строгого слабого упорядочения
    bool operator<(const Point& other) const {
        // Используем std::tie для лексикографического сравнения
        return std::tie(x, y) < std::tie(other.x, other.y);
    }
};

// Использование
std::set<Point> pointSet;
std::map<Point, std::string> pointMap;

2. Для неупорядоченных контейнеров (std::unordered_map, std::unordered_set): Ключ должен быть хешируемым и сравнимым на равенство.

  • Для типа должен быть специализирован std::hash.
  • Должен быть определен оператор operator== или предоставлен предикат равенства.
// Пример ключа для std::unordered_set или std::unordered_map
struct Point {
    int x, y;
    // Определяем оператор ==
    bool operator==(const Point& other) const {
        return x == other.x && y == other.y;
    }
};

// Специализируем std::hash для Point
namespace std {
    template<> struct hash<Point> {
        size_t operator()(const Point& p) const noexcept {
            // Комбинируем хэши полей. Используем XOR и сдвиг для лучшего распределения.
            return hash<int>()(p.x) ^ (hash<int>()(p.y) << 1);
        }
    };
}

// Использование
std::unordered_set<Point> pointUSet;
std::unordered_map<Point, int> pointUMap;

Важно: Для пользовательских типов в качестве ключа в std::unordered_* контейнерах часто также требуется определить operator== в том же пространстве имен, что и сам тип (или в std), чтобы ADL мог его найти.

Ответ 18+ 🔞

А, ну это же классика, ёпта! Сидишь такой, пишешь код, и тут — бац! — надо свою структуру в std::map или unordered_set засунуть. А оно не компилируется, потому что компилятор — пидарас шерстяной — не понимает, как твои кастомные объекты сравнивать или хешировать. Давай разберём, чтобы не было волнения ебать.

Смотри, тут всё зависит от того, в какой контейнер ты лезешь.

1. Для контейнеров, которые всё держат по порядку (map, set, и их многовариантные братья)

Тут ключ должен уметь сравниваться. По-умному это называется «строгое слабое упорядочение». Короче, для твоего типа должен быть operator< или свой компаратор, который объяснит, какой объект «меньше» другого. Без этого — нихуя не получится.

// Допустим, есть точка
struct Point {
    int x, y;
    // Вот тут мы говорим компилятору: "Э, бошка, думай! Вот как сравнивать"
    bool operator<(const Point& other) const {
        // Используем std::tie, чтобы не выёбываться с if-ами
        return std::tie(x, y) < std::tie(other.x, other.y);
    }
};

// И теперь можно юзать
std::set<Point> pointSet; // Всё, работает
std::map<Point, std::string> pointMap; // Тоже ок

Если operator< не написать, компилятор посмотрит на тебя как на идиота и скажет что-то вроде «не могу сравнить, иди на хуй». Честно.

2. Для неупорядоченных контейнеров (unordered_map, unordered_set)

А вот тут уже цирк, ёперный театр! Порядок не важен, но зато ключ должен быть хешируемым и сравнимым на равенство. То есть:

  1. Нужна специализация std::hash для твоего типа — чтобы можно было получить числовой хеш.
  2. Нужен operator== — чтобы понять, а не один и тот же ли это объект, если хеши совпали (коллизии же бывают, овердохуища).
struct Point {
    int x, y;
    // Сперва учим сравнивать на равенство
    bool operator==(const Point& other) const {
        return x == other.x && y == other.y;
    }
};

// А теперь самое интересное — учим STL хешировать нашу точку
namespace std {
    template<> struct hash<Point> {
        size_t operator()(const Point& p) const noexcept {
            // Берём хеши от полей и как-то их смешиваем. Часто через XOR.
            // Сдвиг тут чтобы для точек (1,0) и (0,1) хеши были разные, а то будет манда с ушами.
            return hash<int>()(p.x) ^ (hash<int>()(p.y) << 1);
        }
    };
}

// И теперь можно!
std::unordered_set<Point> pointUSet;
std::unordered_map<Point, int> pointUMap;

Важный момент, на котором многие обжигаются: для unordered_* контейнеров operator== должен быть виден в нужном пространстве имён (обычно рядом с типом или в std), иначе ADL его не найдёт и будет тихий ужас. Если не сделать — будет тебе хиросима и нигерсраки в лучшем случае, или просто не скомпилируется.

Короче, запомни: для порядка — operator<, для беспорядка — hash + operator==. Всё остальное — от лукавого.