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

«Какие требования предъявляются к классу, чтобы его можно было использовать в качестве ключа в ассоциативных контейнерах C++?» — вопрос из категории C++ Core, который задают на 25% собеседований C/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 мог его найти.