Std:: map Требования к ключам (проектное решение)

Когда я создаю std::map<my_data_type, mapped_value>, то, что С++ ожидает от меня, состоит в том, что my_data_type имеет свой собственный operator<.

struct my_data_type
{
    my_data_type(int i) : my_i(i) { }

    bool operator<(const my_data_type& other) const { return my_i < other.my_i; }

    int my_i;
};

Причина в том, что вы можете получить operator> и operator== из operator<. b < a следует a > b, поэтому там operator>.! (a < b) & &! (b < a) означает, что a не меньше b и не больше, поэтому они должны быть равны.

Возникает вопрос: почему разработчик С++ не требует явного определения operator==? Очевидно, operator== неизбежно для std::map::find() и для удаления дубликатов из std::map. Зачем реализовывать 5 операций и дважды вызывать метод, чтобы не заставить меня явно реализовать operator==?

Ответ 1

operator== неизбежно для std::map::find()

Здесь вы ошибаетесь. map вообще не использует operator==, это не "неизбежно". Две клавиши x и y считаются эквивалентными для целей карты, если !(x < y) && !(y < x).

map не знает или не заботится о том, что вы внедрили operator==. Даже если у вас есть, не обязательно, чтобы все эквивалентные ключи в порядке равны в соответствии с operator==.

Причиной всего этого является то, что везде, где С++ полагается на заказы (сортировка, карты, множества, бинарные поиски), он основывает все, что он делает на хорошо понятой математической концепции "строгого слабого порядка", который также определяется в стандарте. Нет особой необходимости в operator==, и если вы посмотрите на код для этих стандартных функций, вы не очень часто увидите что-нибудь вроде if (!(x < y) && !(y < x)), которое делает оба теста близко друг к другу.

Кроме того, ничто из этого не зависит от operator<. Компаратор по умолчанию для map равен std::less<KeyType>, и по умолчанию используется operator<. Но если у вас есть специализированный std::less для KeyType, вам не нужно определять operator<, и если вы укажете другой компаратор для карты, то он может или не может иметь ничего общего с operator< или std::less<KeyType>. Итак, где я сказал x < y выше, действительно это cmp(x,y), где cmp - строгий слабый порядок.

Эта гибкость - еще одна причина, по которой не перетаскивать в нее operator==. Предположим, что KeyType - std::string, и вы указываете свой собственный компаратор, который реализует какие-то специфические для региона правила, не зависящие от регистра. Если map использовал operator== некоторое время, то это полностью игнорировало бы тот факт, что строки, отличающиеся только случаем, должны считаться одним и тем же ключом (или на некоторых языках: с другими различиями, которые считаются не имеющими значения для целей сопоставления). Таким образом, сравнение равенства также должно быть настраиваемым, но есть только один "правильный" ответ, который может предоставить программист. Это не очень хорошая ситуация, вы никогда не хотите, чтобы ваш API предлагал что-то, что похоже на точку настройки, но на самом деле это не так.

Кроме того, концепция заключается в том, что после того, как вы исключили раздел дерева, который меньше, чем ключ, который вы ищете, и раздел дерева, для которого ключ меньше его, то, что осталось либо empty (нет совпадения найдено), иначе у него есть ключ (совпадение найдено). Итак, вы уже использовали current < key, затем key < current, не оставив никакой другой опции, кроме эквивалентности. Ситуация точно:

if (search_key < current_element)
    go_left();
else if (current_element < search_key)
    go_right();
else
    declare_equivalent();

и что вы предлагаете:

if (search_key < current_element)
    go_left();
else if (current_element < search_key)
    go_right();
else if (current_element == search_key)
    declare_equivalent();

что, очевидно, не требуется. На самом деле, это ваше предложение, что менее эффективно!

Ответ 2

Ваши предположения неверны. Вот что на самом деле происходит:

std::map - это шаблон класса, который принимает четыре параметра шаблона: тип ключа K, сопоставленный тип T, компаратор Comp и распределитель Alloc (имена несущественны, конечно, и только локально этот ответ). Для этой дискуссии важно, чтобы объект Comp comp; можно было вызвать с двумя ключевыми рефренями, comp(k1, k2), где k1 и k2 являются K const &, а результат является булевым, который фиксирует строгий слабый порядок.

Если вы не укажете третий аргумент, тогда Comp является типом по умолчанию std::less<K>, и этот (безстоящий) класс выполняет двоичную операцию как k1 < k2. Не имеет значения, является ли этот < -оператор членом K, или свободной функцией, или шаблоном, или что-то еще.

И это завершает историю. Тип компаратора является единственной базой данных, необходимой для реализации упорядоченной карты. Равенство определено как !comp(a, b) && !comp(b,a), и карта сохраняет только один уникальный ключ в соответствии с этим определением равенства.

Нет причин устанавливать дополнительные требования к типу ключа, а также нет логической причины, что пользовательские operator== и operator< должны быть совместимы. Они оба могут существовать независимо друг от друга и служить совершенно другой и не связанной с этим цели.

Хорошая библиотека накладывает минимальные необходимые требования и предлагает максимально возможную гибкость, и это именно то, что делает std::map.

Ответ 3

Чтобы найти элемент i внутри карты, мы перешли к элементу e, поиск дерева уже протестировал i < e, который вернул бы false.

Таким образом, вы вызываете i == e или вызываете e < i, оба из которых подразумевают одно и то же, учитывая предпосылки для нахождения e в дереве. Поскольку нам уже приходилось иметь operator<, мы не полагаемся на operator==, так как это увеличило бы требования ключевой концепции.

Ответ 4

Причиной, по которой нужен оператор сравнения, является способ реализации карты: как дерево двоичное дерево поиска, которое позволяет вам искать, вставить и удалить элементы в O(log n). Чтобы построить это дерево, необходимо задать строгий слабый порядок для набора ключей. Вот почему требуется только одно определение оператора.

Ответ 5

У вас есть ошибочное предположение:

!(a < b) && !(b < a) означает, что a не меньше b и не больше, поэтому они должны быть равны.

Это означает, что они эквивалентны, но не обязательно равны. Вы можете реализовать operator< и operator== таким образом, чтобы два объекта могли быть эквивалентными, но не равными.

Почему разработчику С++ не требуется явно указать operator==?

Чтобы упростить реализацию типов, которые можно использовать в качестве ключей, и позволить вам использовать один пользовательский компаратор для типов без перегруженных операторов. Единственное требование заключается в том, что вы поставляете компаратор (либо operator<, либо пользовательский функтор), который определяет частичный порядок. Ваше предложение потребует как дополнительной работы по реализации сравнения равенства, так и дополнительного ограничения необходимости эквивалентных эквивалентных объектов.