Есть = = а!= взаимозависимы?


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

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

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

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

15 289

15 ответов:

вы не хотите, чтобы язык автоматически переписывался a != b Как !(a == b), когда a == b возвращает что-то другое, чем a 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), поэтому необходимы отдельные определения.

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

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

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

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

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

вы не должны определить.
Если они взаимоисключающие, вы все равно можете быть кратким, только определяя == и < вместе с 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 );
}

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

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

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

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

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

одна из причин, по которой имеет смысл позволить разработчику перегружать оба, - это производительность. Вы можете разрешить оптимизацию, реализовав оба == и !=. Тогда 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'

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

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

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

в ответ на изменения;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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