Чи взаємозалежні == і! =?


292

Я дізнаюся про перевантаження оператора в C ++, і я бачу це ==і !=просто деякі спеціальні функції, які можна налаштувати під визначені користувачем типи. Але я хвилююся, чому потрібні два окремі визначення? Я подумав, що якщо a == bце правда, то a != bавтоматично помилково, і навпаки, а іншої можливості немає, тому що, за визначенням, a != bє !(a == b). І я не міг уявити жодної ситуації, в якій це не було правдою. Але, можливо, моя уява обмежена або я щось не знаю?

Я знаю, що я можу визначити одне з точки зору іншого, але про це я не прошу. Я також не запитую про різницю між порівнянням об'єктів за значенням чи за особою. Або два об'єкти могли бути однаковими і нерівними одночасно (це однозначно не варіант! Ці речі є взаємовиключними). Про що я прошу:

Чи є ситуації , в яких можна задавати питання про двох об'єктів чи рівні має сенс, але питати про них НЕ рівні не має сенсу? (або з точки зору користувача, або з точки зору реалізації)

Якщо такої можливості немає, то чому на Землі C ++ визначає ці два оператори як дві різні функції?


13
Два вказівники можуть бути нульовими, але необов'язково рівними.
Алі Каглаян

2
Не впевнений, чи є сенс тут, але прочитавши це, змусив мене задуматися про проблеми «короткого замикання». Наприклад, можна визначити, що 'undefined' != expressionзавжди є правдивим (або хибним, або невизначеним), незалежно від того, чи можна оцінити вираз. У цьому випадку a!=bповертається правильний результат за визначенням, але !(a==b)не вдається, якщо bйого неможливо оцінити. (Або зайняти багато часу, якщо оцінка bдорога).
Денніс Джахеруддін

2
А що з null! = Null і null == null? Це може бути і те, і інше, так що якщо a! = B, це не завжди означає a == b.
зозо

4
Приклад з javascript(NaN != NaN) == true
chiliNUT

Відповіді:


272

Ви б не хотіли, щоб мова автоматично переписувалася, 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)потрібні окремі визначення.


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

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

Чи можлива ситуація, коли задавати питання щодо двох рівних об'єктів має сенс, але запитувати про їх нерівності немає сенсу?

Ми часто пов'язуємо цих операторів з рівністю.
Хоча саме так вони поводяться на основних типах, немає жодного зобов’язання, щоб це було їх поведінкою щодо користувацьких типів даних. Вам навіть не потрібно повертати булінг, якщо ви цього не хочете.

Я бачив, як люди перевантажують операторів химерними способами, лише знаходять, що це має сенс для їхнього доменного застосування. Навіть якщо інтерфейс показує, що вони взаємовиключні, автор може захотіти додати конкретну внутрішню логіку.

(або з точки зору користувача, або з точки зору реалізації)

Я знаю, що ви хочете конкретного прикладу,
тому ось один із рамок тестування 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 Погоджений, я просто кажу, що його може використовувати метод ОП. Я не знаю, як пояснити rel_ops простіше, ніж показано на прикладі. Я посилався на те, де він знаходиться, але розміщував код, оскільки довідкова сторінка завжди могла змінюватися.
Тревор Хікі

4
Вам все одно потрібно зрозуміти, що приклад коду становить 99% від cppreference, а не вашого власного.
ТК

2
Std :: relops, здається, вийшов з ласки. Ознайомтеся з підсилювальними програмами на щось більш націлене.
JDługosz

43

Існують деякі дуже усталені конвенції, в яких (a == b)і обидва(a != b) є помилковими, не обов'язково протилежними. Зокрема, у SQL будь-яке порівняння з NULL дає NULL, а не true та false.

Напевно, не годиться створювати нові приклади цього, якщо це взагалі можливо, тому що це настільки неінтуїтивно, але якщо ви намагаєтесь моделювати існуючу конвенцію, непогано мати можливість змусити ваших операторів вести себе «правильно» для цього контекст.


4
Реалізація SQL-подібної нульової поведінки в C ++? Ewwww. Але я вважаю, що це не те, що, на мою думку, повинно бути заборонено мовою, якою б неприємною вона не була.

1
@ dan1111 Що ще важливіше, деякі аромати SQL цілком можуть бути закодовані в c ++, тому мова повинна підтримувати їх синтаксис, ні?
Джо

1
Виправте мене, якщо я помиляюся, я просто виходжу з вікіпедії тут, але чи не порівняння зі значенням NULL у поверненні SQL Невідомо, не помилково? І чи не заперечення Невідомого ще невідоме? Отже, якщо логіка SQL була закодована в C ++, чи не хочете ви NULL == somethingповернути Unknown, і ви також хочете NULL != somethingповернути Unknown, і ви хочете !Unknownповернутися Unknown. І в цьому випадку реалізація operator!=як заперечення operator==все ще правильна.
Бенджамін Ліндлі

1
@Barmar: Гаразд, але як же це зробити вислів "SQL NULLs працює таким чином" правильним? Якщо ми обмежуємо реалізацію оператора порівняння поверненням булів, чи це не означає, що реалізація логіки SQL з цими операторами неможлива?
Бенджамін Ліндлі

2
@Barmar: Ну ні, це не сенс. ОП вже знає цей факт, або це питання не існувало б. Суть полягала в тому, щоб навести приклад, коли було доцільно або 1) реалізувати один, operator==або operator!=, а не інший, або 2) реалізувати operator!=іншим способом, ніж заперечення operator==. І реалізація логіки SQL для значень NULL не є випадком цього.
Бенджамін Ліндлі

23

Я відповім лише на другу частину вашого питання, а саме:

Якщо такої можливості немає, то чому на Землі 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'

Це, мабуть, буде вважатися мікрооптимізацією, але це може бути гарантовано в деяких випадках.


3
SSE (x86 SIMD) обгорткові класи - прекрасний приклад цього. Існує pcmpeqbінструкція, але жодна упакована-порівняльна інструкція, що створює маску!! Отже, якщо ви не можете просто змінити логіку того, що використовує результати, вам доведеться скористатися іншою інструкцією, щоб перевернути його. (Веселий факт: набір інструкцій AMOP для AMD насправді упакував-порівнював neq. Шкода, що Intel не прийняв / не розширив XOP; в цьому розширенні ISA, що скоро буде мертвим, є кілька корисних інструкцій.)
Пітер Кордес,

1
Весь сенс SIMD в першу чергу - це продуктивність, і ви, як правило, намагаєтеся використовувати його вручну в петлях, важливих для загальної перф. Збереження єдиної інструкції ( PXORз усіма, щоб перевернути результат порівняння маски) у тісний цикл може мати значення.
Пітер Кордес

Продуктивність як причина не є достовірною, коли накладні витрати є одним логічним запереченням .
Ура та хт. - Альф

Це може бути більше одного логічного заперечення, якщо обчислення x == yкоштують значно більше, ніж x != y. Обчислення останнього може бути значно дешевшим через прогнозування галузей тощо.
Сентриль,

16

Чи можлива ситуація, коли задавати питання щодо двох рівних об'єктів має сенс, але запитувати про їх нерівності немає сенсу? (або з точки зору користувача, або з точки зору реалізації)

Це думка. Можливо, це не так. Але мовні дизайнери, не будучи всезнаючими, вирішили не обмежувати людей, які можуть зіткнутися з ситуаціями, в яких це може мати сенс (принаймні, для них).


13

У відповідь на редагування;

Тобто, якщо для певного типу є оператор, ==але не той !=, або навпаки, і коли це має сенс робити.

В загальному , ні, це не має сенсу. Оператори рівності та реляції зазвичай бувають множинами. Якщо є рівність, то і нерівність; менше, то більше, і т. д. і <=т. д. Аналогічний підхід застосовується і до арифметичних операторів, вони також зазвичай входять у природні логічні множини.

Про це свідчить std::rel_opsпростір імен. Якщо ви реалізуєте рівність і менше операторів, використання цього простору імен дає вам інші, реалізовані з точки зору ваших оригінальних реалізованих операторів.

Все, що сказано, чи існують умови чи ситуації, коли одне не одразу означало б інше, або не могло бути реалізоване з точки зору інших? Так, їх , мабуть, мало, але вони є там; знову ж таки, про що свідчить власне rel_opsпростір імен. З цієї причини, дозволяючи реалізовувати їх самостійно, ви можете використовувати мову, щоб отримати потрібну вам або потрібну семантику таким чином, що все ще є природним та інтуїтивним для користувача або клієнта коду.

Лінива оцінка, про яку вже говорилося, є відмінним прикладом цього. Ще одним хорошим прикладом є надання їм семантики, яка зовсім не означає рівність або нерівність. Аналогічним прикладом цього є оператори зсуву бітів <<і >>використовуються для вставки та вилучення потоків. Хоча це може нахмуритися в загальних колах, у деяких областях, певних областях, це може мати сенс.


12

Якщо ==і !=оператори фактично не мають на увазі рівність, таким же чином , що <<і >>оператори потоку не припускають бітовий зсув. Якщо ви ставитесь до символів так, ніби вони означають якусь іншу концепцію, вони не повинні бути взаємовиключними.

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


7

З великою потужністю виходить чудово відповідально, або, принаймні, справді гарне керівництво стилем.

==і !=може бути перевантажено робити все, що завгодно. Це і благо, і прокляття. Гарантії це не !=означає !(a==b).


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

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

Я не можу виправдати перевантаження цього оператора, але у наведеному вище прикладі неможливо визначити operator!=як "протилежне" від operator==.



1
@Snowman: Дафанг не каже, що це добре перерахування (ні гарна ідея визначити таке перерахування), це лише приклад для ілюстрації пункту. З цим (можливо, поганим) визначенням оператора, то !=насправді не означало б протилежне ==.
AlainD

1
@AlainD Ви натиснули посилання, яке я опублікував, і чи знаєте ви цілі цього сайту? Це називається "гумор".

1
@Snowman: Я, звичайно, ... пробачте, я пропустив, що це посилання і призначений як іронія! : 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)не буде оцінено.
Бенджамін Ліндлі

Це, однак, поганий приклад. Коротке замикання не додає тут ніякої магічної переваги.
Олівер Чарльворт

@Oliver Charlesworth Alone це не так, але при з'єднанні з окремими операторами це робить: У випадку ==, він припинить порівнювати, як тільки перші відповідні елементи будуть нерівними. Але у випадку !=, якщо б це було реалізовано з точки зору ==, потрібно спочатку порівняти всі відповідні елементи (коли всі вони рівні), щоб мати змогу сказати, що вони нерівні: P Але коли реалізується як у у наведеному вище прикладі він припинить порівнювати, як тільки знайде першу нерівну пару. І справді чудовий приклад.
BarbaraKwarc

@BenjaminLindley Щоправда, мій приклад був повною нісенітницею. На жаль, я не можу придумати ще один атм, тут вже пізно.
Даніель Жур

1
@BarbaraKwarc: !((a == b) && (c == d))і (a != b) || (c != d)еквівалентні за ефективністю короткого замикання.
Олівер Чарлсворт

2

Налаштовуючи поведінку операторів, ви можете змусити їх робити те, що ви хочете.

Ви можете налаштувати речі. Наприклад, ви можете налаштувати клас. Об'єкти цього класу можна порівняти, просто перевіривши певну властивість. Знаючи, що це так, ви можете написати певний код, який перевіряє лише мінімальні речі, замість того, щоб перевіряти кожен окремий біт кожного окремого властивості у всьому об'єкті.

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

Відповідь hvd містить кілька додаткових прикладів.


2

Так, оскільки один означає "еквівалент", а інший означає "нееквівалентний", і ці терміни є взаємовиключними. Будь-яке інше значення для цього оператора є заплутаним і його слід уникати будь-якими способами.


Вони не є взаємовиключними для всіх випадків. Наприклад, дві нескінченності одночасно не рівні між собою і не рівні одна одній.
владон

@vladon може використовувати використання одного замість іншого в загальному випадку ? Ні. Це означає, що вони просто не рівні. Все інше переходить до спеціальної функції, а не до оператора == /! =
oliora

@vladon, будь ласка, замість загального випадку прочитайте всі випадки моєї відповіді.
оліора

@vladon Наскільки це правда в математиці, ви можете навести приклад, де a != b!(a == b) з цієї причини не дорівнює C?
nitro2k01

2

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

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

Якщо ви хочете змінити логічні символи! ([A] || [B]) логічно стає ([! A] & [! B])
Thijser

Зауважте, що тип повернення operator==()та operator!=()не обов'язково bool, вони можуть бути перерахунком, який включає без громадянства, якби ви цього хотіли, і все-таки оператори все ще можуть бути визначені таким чином (a != b) == !(a==b)має місце ..
lorro
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.