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

292

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

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

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

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

BarbaraKwarc
источник
13
Два указателя могут быть нулевыми, но не обязательно равными.
Али Чаглайан
2
Не уверен, имеет ли это здесь смысл, но чтение этого заставило меня задуматься о проблемах «короткого замыкания». Например, можно определить, что 'undefined' != expressionэто всегда true (или false, или undefined), независимо от того, можно ли вычислить выражение. В этом случае a!=bвернул бы правильный результат согласно определению, но !(a==b)потерпел бы неудачу, если bне может быть оценен. (Или займет много времени, если оценка bстоит дорого).
Деннис Джаэруддин
2
А как насчет нуля! = Нуля и нуля == нуля? Это может быть как ... так что если a! = B, это не всегда означает a == b.
Зозо
4
Пример из javascript(NaN != NaN) == true
chiliNUT

Ответы:

272

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

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

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

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

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


источник
72
Построение выражений - это фантастический практический пример того, когда вы этого хотите, который не зависит от надуманных сценариев.
Оливер Чарльзуорт
6
Другим хорошим примером были бы векторные логические операции. Тогда вы бы предпочли один проход через вычисления данных !=вместо двух проходов . Особенно в то время, когда нельзя было полагаться на компилятор, чтобы объединить циклы. Или даже сегодня, если вам не удастся убедить компилятор, ваши векторы не пересекаются. ==!
41
«Вы можете иметь выражение строитель объектов» - хорошо , то оператор !может также построить некоторое выражение узел , и мы все еще в порядке замены a != bс !(a == b), до сих пор , как идет. То же самое касается lazy<bool>::operator!, он может вернуться lazy<bool>. optional<bool>является более убедительным, поскольку логическая истинность, например, boost::optionalзависит от того, существует ли значение, а не от самого значения.
Стив Джессоп
42
Все это, и Nans - пожалуйста, помните NaNs;
Jsbueno
9
@jsbueno: далее было отмечено, что NaN не являются особенными в этом отношении.
Оливер Чарльзуорт
110

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

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

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

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

сорокопут
источник
18
Это единственный ответ, который имеет практический смысл.
Sonic Atom
2
Мне кажется, что у вас есть причина и следствие в обратном направлении. Вы можете перегрузить их отдельно , так ==и !=существуют в виде различных операторов. С другой стороны, они, вероятно, не существуют как отдельные операторы, потому что вы можете перегружать их отдельно, но из-за устаревших и удобных (краткость кода) причин.
nitro2k01
60

Однако меня беспокоит, почему нужны два отдельных определения?

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

Соотношение:

#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';
}

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

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

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

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

Я знаю, что вы хотите конкретный пример,
так что вот один из фреймворков Catch для тестирования, который я считаю практичным:

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 );
}

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

Тревор Хикки
источник
14
Боже мой, как я мог не знать о std::rel_ops? Большое спасибо за указание на это.
Даниэль Жур
5
Почти дословные копии из cppreference (или где-либо еще) должны быть четко помечены и должным образом обозначены. rel_opsвсе равно ужасно
TC
@TC Согласен, я просто говорю, что метод, который OP может использовать. Я не знаю, как объяснить rel_ops проще, чем показанный пример. Я связался с тем, где он находится, но выложил код, поскольку ссылочная страница всегда могла измениться.
Тревор Хикки
4
Вам все еще нужно прояснить, что пример кода на 99% основан на cppreference, а не на вашем.
TC
2
Std :: relops, похоже, потерял популярность. Проверьте повышение оперативности для чего-то более целенаправленного.
JDługosz
43

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

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

Jander
источник
4
Реализация SQL-подобного нулевого поведения в C ++? Ewwww. Но я полагаю, что это не то, что, по моему мнению, должно быть запрещено в языке, каким бы неприятным оно ни было.
1
@ dan1111 Что более важно, некоторые разновидности SQL могут быть хорошо написаны на c ++, поэтому язык должен поддерживать их синтаксис, не так ли?
Джо
1
Поправьте меня, если я ошибаюсь, я просто выхожу из википедии здесь, но разве сравнение со значением NULL в SQL не возвращает Unknown, not False? И разве отрицание Неизвестного все еще неизвестно? Таким образом, если бы логика SQL была закодирована в C ++, вы бы не хотели NULL == somethingвозвращать Unknown, и вы также хотели NULL != somethingбы возвращать Unknown, и вы хотели бы !Unknownвернуть Unknown. И в этом случае реализация operator!=как отрицание operator==все еще правильна.
Бенджамин Линдли
1
@ Бармар: Хорошо, но тогда как это делает утверждение «SQL NULLs работают таким образом» правильно? Если мы ограничиваем наши реализации операторов сравнения возвратом логических значений, не означает ли это, что реализация логики SQL с этими операторами невозможна?
Бенджамин Линдли
2
@ Бармар: Ну нет, это не главное. ОП уже знает этот факт, иначе этот вопрос не существовал бы. Цель состояла в том, чтобы представить пример, в котором имеет смысл либо 1) реализовать одно из, operator==либо operator!=, но не другое, либо 2) реализовать operator!=способом, отличным от отрицания operator==. И реализация логики SQL для значений NULL не является таковой.
Бенджамин Линдли
23

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

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

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

Даже в Хаскеле, где разработчики очень серьезно относятся к законам и математическим концепциям, все равно разрешается перегружать и то, ==и другое /=, как вы можете видеть здесь ( 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'

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

Centril
источник
3
Классы оболочки SSE (x86 SIMD) являются отличным примером этого. Там есть pcmpeqbинструкция, но нет инструкции упакованного сравнения, создающей маску! =. Поэтому, если вы не можете просто изменить логику того, что использует результаты, вы должны использовать другую инструкцию, чтобы инвертировать его. (Забавный факт: у набора инструкций AMD XOP действительно есть упакованное сравнение neq. Жаль, что Intel не приняла / не расширила XOP; есть некоторые полезные инструкции в этом ISA-расширении, которое скоро станет мертвым.)
Питер Кордес
1
Смысл SIMD, в первую очередь, заключается в производительности, и вы, как правило, потрудитесь использовать его вручную только в циклах, которые важны для общей производительности. Сохранение одной инструкции ( PXORсо всеми единицами для инвертирования результата маски сравнения) в узком цикле может иметь значение.
Питер Кордес
Производительность как причина не заслуживает доверия, когда накладные расходы являются одним логическим отрицанием .
ура и hth. - Альф
Может быть более одного логического отрицания, если вычисления x == yстоят значительно больше, чем x != y. Вычисление последнего может быть значительно дешевле из-за предсказания ветвлений и т. Д.
Centril
16

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

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

Бенджамин Линдли
источник
13

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

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

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

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

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

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

Найл
источник
12

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

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

Taywee
источник
7

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

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

It'sPete
источник
6
enum BoolPlus {
    kFalse = 0,
    kTrue = 1,
    kFileNotFound = -1
}

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

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

Дафанг Цао
источник
1
@ Снеговик: Дафанг не говорит, что это хорошее перечисление (или хорошая идея для определения такого перечисления), это просто пример, иллюстрирующий точку зрения. С этим (возможно, плохим) определением оператора, !=это действительно не означает противоположность ==.
AlainD
1
@AlainD Вы щелкнули ссылку, которую я разместил, и знаете ли вы цель этого сайта? Это называется "юмор".
1
@ Снеговик: я конечно делаю ... извините, я пропустил, что это была ссылка и предназначалась как ирония : o)
AlainD
Подожди, ты перегружаешь унар ==?
LF
5

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

Анирудх Сохил
источник
4

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

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

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

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

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

Даниэль Жур
источник
4
Как !((a == rhs.a) && (b == rhs.b))не допускается короткое замыкание? если !(a == rhs.a), то (b == rhs.b)не будет оцениваться.
Бенджамин Линдли
Это плохой пример. Короткое замыкание не добавляет здесь магического преимущества.
Оливер Чарльзуорт
@ Оливер Чарльзуорт В одиночку это не так, но при объединении с отдельными операторами он делает: В случае ==, он прекратит сравнение, как только первые соответствующие элементы не будут равны. Но в случае !=, если бы он был реализован в терминах ==, ему сначала нужно было бы сравнить все соответствующие элементы (когда они все равны), чтобы можно было сказать, что они не равны: P Но при реализации, как в В приведенном выше примере он прекратит сравнение, как только найдет первую неравную пару. Отличный пример действительно.
BarbaraKwarc
@BenjaminLindley Правда, мой пример был полной чушью. К сожалению, я не могу придумать другой банкомат, здесь слишком поздно.
Даниэль Жур
1
@BarbaraKwarc: !((a == b) && (c == d))и (a != b) || (c != d)эквивалентны с точки зрения эффективности короткого замыкания.
Оливер Чарльзуорт
2

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

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

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

Ответ HVD предоставляет несколько дополнительных примеров.

TOOGAM
источник
2

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

oliora
источник
Они не являются взаимоисключающими для всех случаев. Например, две бесконечности оба не равны друг другу и не равны друг другу.
влад
@vladon может использовать использование одного вместо другого в общем случае ? Нет. Это означает, что они просто не равны. Все остальное идет к специальной функции, а не к оператору == /! =
oliora
@vladon пожалуйста, вместо общего случая прочитайте все случаи в моем ответе.
Олиора
@vladon Столько, сколько это верно в математике, можете ли вы привести пример, где по C a != bэто не равно по !(a == b)этой причине?
nitro2k01
2

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

if( !(a == b || a != b) ){
    // Stateless
}
ToñitoG
источник
Если вы хотите переставить логические символы, то! ([A] || [B]) логически становится ([! A] & [! B])
Thijser
Обратите внимание, что возвращаемый тип operator==()и operator!=()не обязательно bool, они могут быть перечислением, которое включает в себя состояние без состояния, если вы этого хотите, и все же операторы могут все еще быть определены, так что (a != b) == !(a==b)держится ..
Лорро