Какие требования к компаратору (предикату) для std::map?

Ответ

Ключи в std::map<K, V, Compare> должны быть сравнимы с помощью функционального объекта Compare. Этот компаратор должен обеспечивать строгое слабое упорядочение (strict weak ordering).

Основные требования:

  1. Для любых ключей a и b должно выполняться ровно одно из: comp(a, b) == true, comp(b, a) == true или a и b эквивалентны (ни одно условие не истинно).
  2. Отношение должно быть транзитивным: если comp(a, b) == true и comp(b, c) == true, то comp(a, c) == true.
  3. Эквивалентность (отсутствие порядка) также должна быть транзитивной.

Стандартный компаратор std::less<Key> использует operator<. Для пользовательских типов нужно обеспечить такое сравнение.

Примеры реализации:

#include <map>
#include <tuple>
#include <string>

// Способ 1: Перегрузка operator< для типа ключа (наиболее частый)
struct Person {
    std::string name;
    int id;
    // Определяем порядок: сначала по name, затем по id
    bool operator<(const Person& other) const {
        return std::tie(name, id) < std::tie(other.name, other.id);
    }
};
// Теперь можно использовать std::map<Person, Value> без явного указания компаратора.

// Способ 2: Пользовательский функтор-компаратор
struct CaseInsensitiveCompare {
    bool operator()(const std::string& a, const std::string& b) const {
        // Сравнение строк без учета регистра
        return std::lexicographical_compare(
            a.begin(), a.end(),
            b.begin(), b.end(),
            [](char c1, char c2) { return std::tolower(c1) < std::tolower(c2); }
        );
    }
};
// Использование с явным указанием компаратора
std::map<std::string, int, CaseInsensitiveCompare> case_insensitive_map;

// Способ 3: Лямбда-функция (начиная с C++20 для конструктора map)
auto cmp = [](int a, int b) { return a > b; }; // Порядок по убыванию
std::map<int, std::string, decltype(cmp)> descending_map(cmp);

Важно: Несоблюдение строгого слабого упорядочения (например, использование operator<=) ведет к неопределенному поведению.

Ответ 18+ 🔞

Давай разберёмся с этим std::map, а то у некоторых товарищей тут, бля, доверия ебать ноль к собственным компараторам. Ситуация проще, чем кажется, если не накосячить с самого начала.

Короче, твой компаратор — это такой хитрая жопа, которая решает, какой ключ в дереве куда встанет. Главное требование — строгое слабого упорядочение. Звучит страшно, а на деле всё просто: твоя функция сравнения должна работать как нормальный, адекватный operator<.

Три кита, на которых всё держится, ёпта:

  1. Однозначность. Для любых двух ключей a и b должно быть ясно: либо a меньше b, либо b меньше a, либо они считаются равными (в смысле порядка, не в смысле operator==!). Два ключа равны, если comp(a, b) == false и comp(b, a) == false. И всё, третьего не дано.
  2. Транзитивность. Если a меньше b, а b меньше c, то и a обязан быть меньше c. Логично же, блядь.
  3. Транзитивность эквивалентности. Если a равен b, и b равен c, то и a равен c. А то начнётся пиздопроебибна в дереве, и никто ничего не найдёт.

Стандартный компаратор std::less<Key> просто юзает твой operator<. Если его нет — пиши пропало, компилятор тебе мозги вынесет.

Вот тебе три рабочих способа, как это запилить:

#include <map>
#include <tuple>
#include <string>

// Способ 1: Просто и правильно. Перегрузил operator< и похуй.
struct Person {
    std::string name;
    int id;
    // Сравниваем сначала по имени, потом по айдишнику. Красота.
    bool operator<(const Person& other) const {
        return std::tie(name, id) < std::tie(other.name, other.id);
    }
};
// Всё, теперь юзай std::map<Person, Value> и не парься.

// Способ 2: Свой собственный функтор, если стандартное сравнение не катит.
struct CaseInsensitiveCompare {
    bool operator()(const std::string& a, const std::string& b) const {
        // Сравниваем строки, не обращая внимания на регистр. Удобно.
        return std::lexicographical_compare(
            a.begin(), a.end(),
            b.begin(), b.end(),
            [](char c1, char c2) { return std::tolower(c1) < std::tolower(c2); }
        );
    }
};
// Явно говорим мапе, какой компаратор использовать.
std::map<std::string, int, CaseInsensitiveCompare> case_insensitive_map;

// Способ 3: Модно и молодёжно — лямбда (C++20 и выше).
auto cmp = [](int a, int b) { return a > b; }; // Развернули порядок, теперь по убыванию.
std::map<int, std::string, decltype(cmp)> descending_map(cmp);

И главное предупреждение, на котором все обжигаются: если твой компаратор не обеспечивает это самое строгое слабое упорядочение (например, используешь operator<= или какую-то свою кривую логику), то это неопределённое поведение. Мапа превратится в тыкву, поиск сломается, дерево может зациклиться — в общем, будет тебе хиросима, а не контейнер. Компилятор тебя не остановит, но потом будешь сидеть и ебать копать, почему ничего не работает. Так что, чувак, делай по правилам.