Являются `==` и `! =` Взаимозависимыми?

Я изучаю перегрузку оператора на С++, и я вижу, что == и != - это просто некоторые специальные функции, которые могут быть настроены для пользовательских типов. Однако моя забота состоит в том, почему нужны два отдельных определения? Я думал, что если a == b истинно, то a != b автоматически ошибочно, и наоборот, и нет другой возможности, потому что по определению a != b есть !(a == b). И я не мог представить ни одной ситуации, в которой это было бы неверно. Но, может быть, мое воображение ограничено или я ничего не знаю о чем-то?

Я знаю, что я могу определить один в терминах другого, но это не то, о чем я прошу. Я также не спрашиваю о различии между сопоставлением объектов по стоимости или идентификацией. Или два объекта могут быть равными и не равными одновременно (это определенно не вариант! Эти вещи взаимоисключающие). Я спрашиваю об этом:

Есть ли какая-либо ситуация, при которой вопрос о равных двух объектах имеет смысл, но вопрос о том, что они не равны, не имеет смысла? (либо с точки зрения пользователя, либо с точки зрения реализатора).

Если такой возможности нет, то почему на Земле С++ эти два оператора определяются как две различные функции?

Ответ 1

Вы не хотите, чтобы язык автоматически переписывал a != b как !(a == b), когда a == b возвращает что-то, отличное от bool. И есть несколько причин, по которым вы можете сделать это.

У вас могут быть объекты построителя выражений, где a == b не выполняет и не предназначен для сравнения, а просто создает некоторое выражение node, представляющее a == b.

У вас может быть ленивая оценка, где a == b не предназначен и не предназначен для прямого сравнения, а вместо этого возвращает какой-то lazy<bool>, который может быть преобразован в bool неявно или явно в какой-то более поздней версии чтобы фактически выполнить сравнение. Возможно, в сочетании с объектами построителя выражений, чтобы обеспечить полную оптимизацию выражения перед оценкой.

У вас может быть какой-то пользовательский класс шаблона optional<T>, где заданы необязательные переменные t и u, вы хотите разрешить t == u, но верните его optional<bool>.

Вероятно, больше того, о чем я не думал. И хотя в этих примерах операции a == b и a != b имеют смысл, еще a != b - это не то же самое, что !(a == b), поэтому необходимы отдельные определения.

Ответ 2

Если такой возможности нет, то почему на Земле С++ эти два оператора определяются как две различные функции?

Потому что вы можете перегрузить их, и, перегружая их, вы можете дать им совершенно другое значение из своего оригинального.

Возьмем, например, оператор <<, изначально побитовый оператор сдвига влево, теперь обычно перегруженный как оператор вставки, как в std::cout << something; совершенно другое значение от первоначального.

Итак, если вы согласны с тем, что значение оператора изменяется при его перегрузке, тогда нет оснований препятствовать пользователю давать смысл оператору ==, что не является точно отрицанием оператора !=, хотя это может запутать.

Ответ 3

Моя забота, однако, почему существуют два отдельных определения?

Вам не обязательно определять, как. Если они являются взаимоисключающими, вы все равно можете быть краткими, только определяя == и < рядом с std:: rel_ops

Fom cppreference:

#include <iostream>
#include <utility>

struct Foo {
    int n;
};

bool operator==(const Foo& lhs, const Foo& rhs)
{
    return lhs.n == rhs.n;
}

bool operator<(const Foo& lhs, const Foo& rhs)
{
    return lhs.n < rhs.n;
}

int main()
{
    Foo f1 = {1};
    Foo f2 = {2};
    using namespace std::rel_ops;

    //all work as you would expect
    std::cout << "not equal:     : " << (f1 != f2) << '\n';
    std::cout << "greater:       : " << (f1 > f2) << '\n';
    std::cout << "less equal:    : " << (f1 <= f2) << '\n';
    std::cout << "greater equal: : " << (f1 >= f2) << '\n';
}

Возможна ли ситуация, в которой задаются вопросы о двух объекты, имеющие равные значения, имеют смысл, но спрашивают о том, что они не являются равный не имеет смысла?

Мы часто связываем эти операторы с равенством. Хотя именно так они ведут себя по фундаментальным типам, нет никаких обязательств, чтобы это было их поведение в пользовательских типах данных. Вам даже не нужно возвращать bool, если вы этого не хотите.

Я видел, как люди перегружают операторов причудливыми способами, только чтобы найти, что это имеет смысл для их конкретного приложения. Даже если интерфейс показывает, что они являются взаимоисключающими, автор может захотеть добавить определенную внутреннюю логику.

(либо с точки зрения пользователя, либо с точки зрения реализации)

Я знаю, что вам нужен конкретный пример,
поэтому вот один из Catch testing framework, который я считал практичным:

template<typename RhsT>
ResultBuilder& operator == ( RhsT const& rhs ) {
    return captureExpression<Internal::IsEqualTo>( rhs );
}

template<typename RhsT>
ResultBuilder& operator != ( RhsT const& rhs ) {
    return captureExpression<Internal::IsNotEqualTo>( rhs );
}

Эти операторы выполняют разные вещи, и нет смысла определять один метод как (не) другого. Причина, по которой это делается, заключается в том, чтобы структура могла распечатать сделанное сравнение. Для этого ему необходимо учесть контекст использования перегруженного оператора.

Ответ 4

Есть очень хорошо установленные соглашения, в которых (a == b) и (a != b) являются , и false не обязательно противоположны. В частности, в SQL любое сравнение с NULL дает NULL, а не true или false.

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

Ответ 5

Я отвечу только на вторую часть вашего вопроса, а именно:

Если такой возможности нет, то почему на Земле С++ эти два оператора определяются как две различные функции?

Одна из причин, почему имеет смысл разрешить разработчику перегружать обе производительность. Вы можете разрешить оптимизацию, реализуя как ==, так и !=. Тогда x != y может быть дешевле, чем !(x == y). Некоторые компиляторы могут оптимизировать его для вас, но, возможно, нет, особенно если у вас есть сложные объекты с большим количеством ветвлений.

Даже в Haskell, где разработчики очень серьезно относятся к законам и математическим понятиям, все равно разрешено перегружать как ==, так и /=, как вы можете видеть здесь (http://hackage.haskell.org/package/base-4.9.0.0/docs/Prelude.html#v:-61--61-):

$ ghci
GHCi, version 7.10.2: http://www.haskell.org/ghc/  :? for help
λ> :i Eq
class Eq a where
  (==) :: a -> a -> Bool
  (/=) :: a -> a -> Bool
        -- Defined in `GHC.Classes'

Это, вероятно, будет считаться микро-оптимизацией, но в некоторых случаях это может быть оправдано.

Ответ 6

Возможна ли ситуация, в которой задаются вопросы о двух объекты, имеющие равные значения, имеют смысл, но спрашивают о том, что они не являются равный не имеет смысла? (либо с точки зрения пользователя, либо с реализатора)

Это мнение. Может быть, нет. Но разработчики языка, не будучи всеведущими, решили не ограничивать людей, которые могли бы придумать ситуации, в которых это могло бы иметь смысл (по крайней мере для них).

Ответ 7

В ответ на редактирование;

То есть, если какой-либо тип может иметь оператор ==, но не !=, или наоборот, и когда это имеет смысл сделать это.

В общем, нет, это не имеет смысла. Равенство и реляционные операторы обычно входят в набор. Если есть равенство, то и неравенство; меньше, чем больше и т.д. с помощью <= и т.д. Подобный подход применяется и к арифметическим операторам, они также обычно входят в естественные логические множества.

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

Что все сказано, существуют условия или ситуации, когда один не будет сразу означать другого или не может быть реализован с точки зрения других? Да, есть, возможно, немного, но они есть; снова, как показано в rel_ops, являющемся собственным пространством имен. По этой причине, позволяя им быть реализованы независимо, вы можете использовать язык для получения семантики, которая вам нужна или нужна, которая по-прежнему естественна и интуитивно понятна для пользователя или клиента кода.

Ленточная оценка, о которой уже упоминалось, является отличным примером этого. Еще один хороший пример - дать им семантику, которая вовсе не означает равенства или равноправия. Аналогичным примером этого являются операторы сдвига битов << и >>, используемые для вставки и извлечения потока. Хотя это может быть неодобрительно в общих кругах, в некоторых областях конкретной области это может иметь смысл.

Ответ 8

Если операторы == и != фактически не подразумевают равенство, то так же, как операторы потока << и >> не предполагают смещения бит. Если вы относитесь к символам так, как будто они означают какую-то другую концепцию, они не обязательно должны быть взаимоисключающими.

В терминах равенства это может иметь смысл, если ваш случай использования гарантирует, что объекты не сопоставимы, поэтому каждое сравнение должно возвращать false (или несопоставимый тип результата, если ваши операторы возвращают non-bool). Я не могу думать о конкретной ситуации, когда это было бы оправданным, но я мог видеть, что это достаточно разумно.

Ответ 9

С большой силой приходит великолепно ответственно или, по крайней мере, действительно хорошие руководства по стилю.

== и != могут быть перегружены, чтобы делать все, что вы хотите. Это и благословение, и проклятие. Там нет гарантии, что != означает !(a==b).

Ответ 10

enum BoolPlus {
    kFalse = 0,
    kTrue = 1,
    kFileNotFound = -1
}

BoolPlus operator==(File& other);
BoolPlus operator!=(File& other);

Я не могу оправдать эту перегрузку оператора, но в приведенном выше примере невозможно определить operator!= как "противоположное" operator==.

Ответ 11

В конце концов, то, что вы проверяете с этими операторами, состоит в том, что выражение a == b или a!= b возвращает логическое значение. Это выражение возвращает логическое значение после сравнения, а не является взаимоисключающим.

Ответ 12

[..] почему существуют два отдельных определения?

Одна вещь, которую следует учитывать, - это возможность реализации одного из этих операторов более эффективно, чем просто использование отрицания другого.

(Мой пример здесь был мусором, но точка все еще стоит, подумайте о фильтрах цветения, например: они позволяют быстро тестировать, если что-то не в наборе, но тестирование, если оно может занять намного больше времени.)

[..] по определению, a != b есть !(a == b).

И это ваша ответственность, как программист, сделать это. Наверное, хорошая вещь, чтобы написать тест.

Ответ 13

Может быть, несравнимое правило, где a!= b было ложным, а == b было ложным, как бит без сохранения.

if( !(a == b || a != b) ){
    // Stateless
}

Ответ 14

Благодаря настройке поведения операторов вы можете заставить их делать то, что вы хотите.

Возможно, вы захотите настроить все. Например, вы можете настроить класс. Объекты этого класса можно сравнить, просто проверив определенное свойство. Зная, что это так, вы можете написать определенный код, который проверяет только минимальные вещи, а не проверяет каждый бит каждого отдельного свойства во всем объекте.

Представьте себе случай, когда вы можете понять, что что-то другое так же быстро, если не быстрее, чем вы можете узнать, что-то одно и то же. Конечно, как только вы выясните, что-то то же или другое, тогда вы можете узнать обратное, просто перевернув немного. Однако перебрасывание этого бита является дополнительной операцией. В некоторых случаях, когда код получает многократное повторное выполнение, сохранение одной операции (умноженной на много раз) может иметь общее увеличение скорости. (Например, если вы сохраняете одну операцию на пиксель мегапиксельного экрана, то вы только что сохранили миллион операций. Умножаетесь на 60 экранов в секунду, и вы сохраняете еще больше операций.)

hvd answer содержит некоторые дополнительные примеры.

Ответ 15

Да, потому что один означает "эквивалент", а другой означает "неэквивалент", и эти условия являются взаимоисключающими. Любое другое значение для этих операторов сбивает с толку, и его следует избегать всеми способами.