Когда класс можно хранить в контейнере std::set?

Ответ

Класс или структура могут быть использованы в качестве ключа std::set, если для них определен строгий слабый порядок (Strict Weak Ordering). Это достигается одним из двух способов:

  1. Определение оператора < для типа ключа.
  2. Предоставление пользовательского функтора-компаратора в качестве второго шаблонного параметра std::set.

Пример 1: Использование оператора <

struct Point {
    int x, y;
    // Определяем порядок: сначала по x, затем по y
    bool operator<(const Point& other) const {
        return std::tie(x, y) < std::tie(other.x, other.y);
    }
};

std::set<Point> points; // Корректно, используется operator<

Пример 2: Использование пользовательского компаратора

struct Person {
    std::string name;
    int age;
};
// Компаратор, сравнивающий только по возрасту
struct CompareByAge {
    bool operator()(const Person& a, const Person& b) const {
        return a.age < b.age;
    }
};

std::set<Person, CompareByAge> people;
// В этом set нельзя будет иметь двух людей с одинаковым возрастом.

Критические требования к компаратору (Compare):

  • Антисимметричность: Если comp(a, b) == true, то comp(b, a) == false.
  • Транзитивность: Если comp(a, b) == true и comp(b, c) == true, то comp(a, c) == true.
  • Транзитивность эквивалентности: Если !comp(a, b) && !comp(b, a) (эквивалентны) и !comp(b, c) && !comp(c, b), то !comp(a, c) && !comp(c, a).

Без выполнения этих условий поведение std::set не определено (UB).

Ответ 18+ 🔞

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

Короче, есть два основных пути, как это провернуть.

Первый — самый простой. Просто нахуяриваешь для своей структуры оператор <. Сделал это — и всё, std::set уже доволен, он сам им пользоваться будет. Смотри, как это выглядит:

struct Point {
    int x, y;
    // Определяем порядок: сначала по x, затем по y
    bool operator<(const Point& other) const {
        return std::tie(x, y) < std::tie(other.x, other.y);
    }
};

std::set<Point> points; // Корректно, используется operator<

Видишь? std::tie — это вообще гениальная штука, чтобы не писать эти ёбаные цепочки if (x != other.x) return x < other.x; else return y < other.y;. Красота, ядрёна вошь!

А второй путь — для хитрожопых ситуаций. Допустим, тебе не нужен стандартный порядок, или оператор < для твоего типа — это пиздец какая мудя. Тогда ты можешь дать set'у свою собственную функцию сравнения. Засовываешь её как второй шаблонный аргумент, и вуаля.

struct Person {
    std::string name;
    int age;
};
// Компаратор, сравнивающий только по возрасту
struct CompareByAge {
    bool operator()(const Person& a, const Person& b) const {
        return a.age < b.age;
    }
};

std::set<Person, CompareByAge> people;
// В этом set нельзя будет иметь двух людей с одинаковым возрастом.

Смотри, какой прикол: в таком сете будут храниться уникальные значения возраста. Двух Вась с возрастом 30 быть не может, даже если у них имена разные. Потому что компаратору на имя вообще похуй, он смотрит только на age. Удивление пиздец, да?

Но вот тут, блядь, самое важное! К твоему компаратору, будь он оператором или отдельной структурой, есть жёсткие требования. Если их нарушить — получишь неопределённое поведение, и всё полетит к чёртовой матери в самый неподходящий момент. Запоминай, что он должен уметь:

  • Не быть ебаньком-оборотнем: Если comp(a, b) сказал true, то comp(b, a) обязан быть false. Не может объект одновременно быть и меньше, и больше другого, это же ёперный театр.
  • Быть последовательным, как параноик: Если a меньше b, а b меньше c, то и a обязан быть меньше c. Без вариантов.
  • Иметь чёткое понятие "равенства": Если a не меньше b и b не меньше a — значит, они для сета эквивалентны. И это отношение тоже должно быть транзитивным.

Нарушишь эти правила — и твой std::set превратится в тыкву, то есть в источник рандомных багов, которые хуй потом отловишь. Так что, чувак, думай головой, когда порядок определяешь.