== e! = Sono reciprocamente dipendenti?


292

Sto imparando circa l'overloading degli operatori in C ++, e vedo che ==e !=sono semplicemente alcune funzioni speciali che possono essere personalizzati per i tipi definiti dall'utente. La mia preoccupazione è, tuttavia, perché sono necessarie due definizioni separate ? Ho pensato che se a == bè vero, allora a != bè automaticamente falso, e viceversa, e non c'è altra possibilità, perché, per definizione, lo a != bè !(a == b). E non riuscivo a immaginare alcuna situazione in cui ciò non fosse vero. Ma forse la mia immaginazione è limitata o ignoro qualcosa?

So di poter definire l'uno in termini di altro, ma non è questo ciò di cui mi sto chiedendo. Inoltre, non sto chiedendo la distinzione tra confrontare oggetti per valore o per identità. O se due oggetti potrebbero essere uguali e non uguali allo stesso tempo (questa non è sicuramente un'opzione! Queste cose si escludono a vicenda). Quello che sto chiedendo è questo:

C'è qualche situazione possibile in cui ha senso porre domande su due oggetti uguali, ma non ha senso chiederle se non sono uguali? (dal punto di vista dell'utente o dal punto di vista dell'implementatore)

Se non esiste tale possibilità, perché sulla Terra il C ++ ha questi due operatori definiti come due funzioni distinte?


13
Due puntatori possono essere entrambi nulli ma non necessariamente uguali.
Ali Caglayan,

2
Non sono sicuro che abbia senso qui, ma leggere questo mi ha fatto pensare a problemi di "corto circuito". Ad esempio, si potrebbe definire che 'undefined' != expressionè sempre vero (o falso o indefinito), indipendentemente dal fatto che l'espressione possa essere valutata. In questo caso a!=brestituirebbe il risultato corretto secondo la definizione, ma !(a==b)fallirebbe se non fosse bpossibile valutarlo. (O impiega molto tempo se la valutazione bè costosa).
Dennis Jaheruddin,

2
Che dire di null! = Null e null == null? Può essere entrambi ... quindi se a! = B ciò non significa sempre a == b.
zozo,

4
Un esempio tratto da javascript(NaN != NaN) == true
chiliNUT,

Risposte:


272

Si potrebbe non voler la lingua per riscrivere automaticamente a != bcome !(a == b)quando a == britorna qualcosa di diverso da un bool. E ci sono alcuni motivi per cui potresti farlo farlo.

Potresti avere oggetti del generatore di espressioni, dove a == bno e non è destinato a eseguire alcun confronto, ma crea semplicemente un nodo di espressione che rappresenta a == b.

Potresti avere una valutazione pigra, dove a == bno e non è prevista l'esecuzione diretta di alcun confronto, ma restituisce invece un tipo di lazy<bool>ciò che può essere convertito in modo boolimplicito o esplicito in un momento successivo per eseguire effettivamente il confronto. Eventualmente combinato con gli oggetti del generatore di espressioni per consentire l'ottimizzazione completa delle espressioni prima della valutazione.

Potresti avere una optional<T>classe di template personalizzata , dove sono fornite variabili opzionali te u, vuoi permetterlo t == u, ma fallo tornare optional<bool>.

Probabilmente c'è altro a cui non ho pensato. E anche se in questi esempi l'operazione a == be a != bentrambe hanno un senso, a != bnon è ancora la stessa cosa !(a == b), quindi sono necessarie definizioni separate.


72
La creazione di espressioni è un fantastico esempio pratico di quando lo vorresti, che non si basa su scenari inventati.
Oliver Charlesworth,

6
Un altro buon esempio potrebbe essere rappresentato dalle operazioni logiche vettoriali. Si preferisce un solo passaggio attraverso i dati di calcolo !=, invece di due passaggi di calcolo ==allora !. Soprattutto nei giorni in cui non si poteva fare affidamento sul compilatore per fondere i loop. O ancora oggi se non riesci a convincere il compilatore i tuoi vettori non si sovrappongono.

41
"Si può avere espressioni oggetti" - beh, allora operatore !può anche costruire qualche nodo espressione e stiamo ancora bene la sostituzione a != bcon !(a == b), per quanto che va. Lo stesso vale per lazy<bool>::operator!, può tornare lazy<bool>. optional<bool>è più convincente, poiché la logica verità di per esempio boost::optionaldipende dal fatto che esista un valore, non dal valore stesso.
Steve Jessop,

42
Tutto ciò, e Nans - per favore ricorda la NaNs;
jsbueno,

9
@jsbueno: è stato sottolineato più in basso che le NaN non sono speciali in questo senso.
Oliver Charlesworth,

110

Se non esiste tale possibilità, perché sulla Terra il C ++ ha questi due operatori definiti come due funzioni distinte?

Perché puoi sovraccaricarli e sovraccaricandoli puoi dare loro un significato totalmente diverso da quello originale.

Prendiamo, ad esempio, l'operatore <<, originariamente l'operatore di spostamento a sinistra bit a bit, ora comunemente sovraccaricato come operatore di inserimento, come in std::cout << something; significato totalmente diverso da quello originale.

Pertanto, se si accetta che il significato di un operatore cambia quando lo si sovraccarica, non vi è motivo di impedire all'utente di dare un significato all'operatore ==che non sia esattamente la negazione dell'operatore !=, sebbene ciò possa creare confusione.


18
Questa è l'unica risposta che ha un senso pratico.
Sonic Atom

2
A me sembra che tu abbia la causa e l'effetto all'indietro. Si possono sovraccaricare separatamente perché ==e !=esistere come operatori distinti. D'altra parte, probabilmente non esistono come operatori distinti perché è possibile sovraccaricarli separatamente, ma per motivi legati alla legacy e alla convenienza (brevità del codice).
nitro2k01,

60

La mia preoccupazione è, tuttavia, perché sono necessarie due definizioni separate?

Non è necessario definire entrambi.
Se si escludono a vicenda, puoi comunque essere conciso solo definendo ==e <affiancando std :: rel_ops

Per riferimento:

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

C'è qualche situazione possibile in cui ha senso porre domande su due oggetti uguali, ma non ha senso chiederle se non sono uguali?

Associamo spesso questi operatori all'uguaglianza.
Sebbene sia così che si comportano su tipi fondamentali, non vi è alcun obbligo che questo sia il loro comportamento su tipi di dati personalizzati. Non devi nemmeno restituire un bool se non vuoi.

Ho visto persone sovraccaricare gli operatori in modi bizzarri, solo per scoprire che ha senso per la loro specifica applicazione di dominio. Anche se l'interfaccia sembra mostrare che si escludono a vicenda, l'autore potrebbe voler aggiungere una logica interna specifica.

(dal punto di vista dell'utente o dal punto di vista dell'implementatore)

So che vuoi un esempio specifico,
quindi eccone uno dal framework di test Catch che pensavo fosse pratico:

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

Questi operatori stanno facendo cose diverse e non avrebbe senso definire un metodo come! (Non) dell'altro. Il motivo è che il framework può stampare il confronto effettuato. Per fare ciò, deve catturare il contesto dell'operatore sovraccarico utilizzato.


14
Oh mio Dio, come potrei non saperlo std::rel_ops? Grazie mille per averlo sottolineato.
Daniel Jour,

5
Le copie quasi verbali da cppreference (o altrove) dovrebbero essere chiaramente contrassegnate e correttamente attribuite. rel_opsè orribile comunque.
TC,

@TC D'accordo, sto solo dicendo che è un metodo che OP può prendere. Non so come spiegare rel_ops in modo più semplice dell'esempio mostrato. Ho collegato a dove si trova, ma il codice pubblicato poiché la pagina di riferimento potrebbe sempre cambiare.
Trevor Hickey,

4
Devi ancora chiarire che l'esempio di codice è del 99% da cppreference, piuttosto che il tuo.
TC,

2
Std :: relops sembra essere caduto in disgrazia. Dai un'occhiata alle operazioni di boost per qualcosa di più mirato.
JDługosz,

43

Ci sono alcune convenzioni ben consolidate in cui (a == b)e (a != b)sono entrambi falsi, non necessariamente opposti. In particolare, in SQL, qualsiasi confronto con NULL produce NULL, non vero o falso.

Probabilmente non è una buona idea creare nuovi esempi di questo, se possibile, perché è così poco intuitivo, ma se stai cercando di modellare una convenzione esistente, è bello avere l'opzione per far sì che i tuoi operatori si comportino "correttamente" per quello contesto.


4
Implementazione del comportamento null di tipo SQL in C ++? Ewwww. Ma suppongo che non sia qualcosa che penso dovrebbe essere vietato nella lingua, per quanto sgradevole possa essere.

1
@ dan1111 Ancora più importante, alcune versioni di SQL potrebbero essere codificate in c ++, quindi il linguaggio deve supportare la loro sintassi, no?
Joe,

1
Correggimi se sbaglio, sto solo uscendo da Wikipedia qui, ma il confronto con un valore NULL in SQL restituisce Unknown, not False? E la negazione di Unknown non è ancora Unknown? Quindi, se la logica SQL fosse codificata in C ++, non si vorrebbe NULL == somethingrestituire Sconosciuto e si vorrebbe anche NULL != somethingrestituire Sconosciuto e si vorrebbe !Unknownrestituire Unknown. E in quel caso l'implementazione operator!=come negazione di operator==è ancora corretta.
Benjamin Lindley,

1
@Barmar: Okay, ma allora come fa a correggere l'istruzione "SQL NULLs in questo modo" ? Se stiamo limitando le implementazioni dei nostri operatori di confronto al ritorno di valori booleani, ciò non significa semplicemente che l'implementazione della logica SQL con questi operatori è impossibile?
Benjamin Lindley,

2
@Barmar: Beh no, non è questo il punto. L'OP lo sa già, o questa domanda non esisterebbe. Il punto era presentare un esempio in cui aveva senso 1) implementare uno operator==o operator!=, ma non l'altro, o 2) implementare operator!=in un modo diverso dalla negazione di operator==. E l'implementazione della logica SQL per i valori NULL non è un caso.
Benjamin Lindley,

23

Risponderò solo alla seconda parte della tua domanda, vale a dire:

Se non esiste tale possibilità, perché sulla Terra il C ++ ha questi due operatori definiti come due funzioni distinte?

Uno dei motivi per cui ha senso consentire allo sviluppatore di sovraccaricare entrambi è la prestazione. È possibile consentire le ottimizzazioni implementando sia ==e !=. Quindi x != ypotrebbe essere più economico di!(x == y) è. Alcuni compilatori potrebbero essere in grado di ottimizzarlo per te, ma forse no, soprattutto se hai oggetti complessi con molte ramificazioni.

Anche a Haskell, dove gli sviluppatori prendono molto sul serio leggi e concetti matematici, si può ancora sovraccaricare entrambi ==e /=, come potete vedere qui ( 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'

Questo sarebbe probabilmente considerato micro-ottimizzazione, ma potrebbe essere giustificato per alcuni casi.


3
Le classi wrapper SSE (x86 SIMD) ne sono un ottimo esempio. C'è pcmpeqbun'istruzione, ma nessuna istruzione di confronto compresso produce una maschera! =. Quindi, se non puoi semplicemente invertire la logica di qualunque cosa usi i risultati, devi usare un'altra istruzione per invertirla. (Fatto curioso: il set di istruzioni XOP di AMD ha un sacco di confronto neq. Peccato che Intel non abbia adottato / esteso XOP; ci sono alcune istruzioni utili in quella futura estensione ISA.)
Peter Cordes,

1
L'intero punto di SIMD è in primo luogo le prestazioni e in genere ti preoccupi solo di usarlo manualmente in loop che sono importanti per la perf complessiva. Il salvataggio di una singola istruzione ( PXORcon tutti per invertire il risultato della maschera di confronto) in un ciclo stretto può essere importante.
Peter Cordes,

Le prestazioni come motivo non sono credibili quando l'overhead è una negazione logica .
Saluti e hth. - Alf

Potrebbe essere più di una negazione logica se il calcolo x == ycosta in modo molto più significativo di x != y. Il calcolo di quest'ultimo potrebbe essere significativamente più economico a causa della previsione del ramo, ecc.
Centril

16

C'è qualche situazione possibile in cui ha senso porre domande su due oggetti uguali, ma non ha senso chiederle se non sono uguali? (dal punto di vista dell'utente o dal punto di vista dell'implementatore)

Questa è un'opinione. Forse no. Ma i progettisti del linguaggio, non essendo onniscienti, hanno deciso di non limitare le persone che potrebbero inventare situazioni in cui potrebbe avere senso (almeno per loro).


13

In risposta alla modifica;

Cioè, se è possibile per un tipo avere l'operatore ==ma non il !=, o viceversa, e quando ha senso farlo.

In generale , no, non ha senso. L'uguaglianza e gli operatori relazionali generalmente si presentano in serie. Se esiste l'uguaglianza, anche la disuguaglianza; meno di, quindi maggiore di e così via con il<= ecc. Un approccio simile viene applicato anche agli operatori aritmetici, che generalmente vengono anche in set logici naturali.

Ciò è evidenziato nel std::rel_ops spazio nomi. Se si implementa l'uguaglianza e meno degli operatori, l'utilizzo di quello spazio dei nomi ti dà gli altri, implementati in termini di operatori implementati originali.

Detto questo, ci sono condizioni o situazioni in cui l'una non significherebbe immediatamente l'altra o non potrebbe essere implementata in termini di altre? Sì, ce ne sono , probabilmente pochi, ma ci sono; di nuovo, come evidenziato nelrel_ops dall'essere uno spazio dei nomi a sé stante. Per questo motivo, consentire loro di essere implementati in modo indipendente consente di sfruttare la lingua per ottenere la semantica richiesta o necessaria in un modo ancora naturale e intuitivo per l'utente o il cliente del codice.

La valutazione pigra già menzionata ne è un eccellente esempio. Un altro buon esempio è dare loro la semantica che non significa affatto uguaglianza o disuguaglianza. Un esempio simile a questo sono gli operatori bit shift <<e >>utilizzati per l'inserimento e l'estrazione del flusso. Sebbene possa essere disapprovato nei circoli generali, in alcune aree specifiche del dominio può avere un senso.


12

Se gli operatori ==e !=non implicano effettivamente l'uguaglianza, allo stesso modo in cui gli operatori <<e >>stream non implicano lo spostamento dei bit. Se tratti i simboli come se significassero qualche altro concetto, non devono escludersi a vicenda.

In termini di uguaglianza, potrebbe avere senso se il tuo caso d'uso garantisca il trattamento di oggetti come non comparabili, in modo che ogni confronto dovrebbe restituire falso (o un tipo di risultato non confrontabile, se i tuoi operatori restituiscono non-bool). Non riesco a pensare a una situazione specifica in cui ciò sarebbe giustificato, ma ho potuto vederlo abbastanza ragionevole.


7

Con una grande potenza arriva una grande responsabilità, o almeno delle ottime guide di stile.

==e !=può essere sovraccarico per fare qualunque diavolo tu voglia. È sia una benedizione che una maledizione. Non c'è garanzia che !=significhi !(a==b).


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

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

Non posso giustificare questo sovraccarico dell'operatore, ma nell'esempio sopra è impossibile definire operator!="l'opposto" di operator==.



1
@Snowman: Dafang non dice che è una buona enumerazione (né una buona idea per definire un'enumerazione come quella), è solo un esempio per illustrare un punto. Con questa (forse cattiva) definizione dell'operatore, allora !=non significherebbe davvero il contrario di ==.
AlainD,

1
@AlainD hai fatto clic sul link che ho pubblicato e sei a conoscenza dello scopo di quel sito? Questo si chiama "umorismo".

1
@Snowman: certamente ... scusami, mi mancava un link e intendevo ironia! : o)
AlainD,

Aspetta, stai sovraccaricando unario ==?
LF

5

Alla fine, ciò che si sta verificando con quegli operatori è che l'espressione a == bo a != bsta restituendo un valore booleano ( trueo false). Questa espressione restituisce un valore booleano dopo il confronto anziché essere reciprocamente esclusive.


4

[..] perché sono necessarie due definizioni separate?

Una cosa da considerare è che potrebbe esserci la possibilità di implementare uno di questi operatori in modo più efficiente rispetto al semplice utilizzo della negazione dell'altro.

(Il mio esempio qui è stato spazzatura, ma il punto è ancora valido, pensate ai filtri di fioritura, ad esempio: consentono test rapidi se qualcosa non è in un set, ma testare se è in può richiedere molto più tempo.)

[..] per definizione, a != bè !(a == b).

Ed è tua responsabilità come programmatore mantenere questa presa. Probabilmente una buona cosa per cui scrivere un test.


4
Come !((a == rhs.a) && (b == rhs.b))non consentire il corto circuito? se !(a == rhs.a), quindi (b == rhs.b)non verrà valutato.
Benjamin Lindley,

Questo è un cattivo esempio, però. Il corto circuito non aggiunge alcun vantaggio magico qui.
Oliver Charlesworth,

@Oliver Charlesworth Da solo non lo fa, ma quando viene unito con operatori separati, lo fa: in caso affermativo ==, smetterà di comparare non appena i primi elementi corrispondenti non saranno uguali. Ma nel caso in cui !=, se fosse implementato in termini di ==, dovrebbe prima confrontare tutti gli elementi corrispondenti (quando sono tutti uguali) per essere in grado di dire che non sono uguali: P Ma quando implementato come in nell'esempio sopra, smetterà di comparare non appena troverà la prima coppia non uguale. Davvero un grande esempio.
BarbaraKwarc,

@BenjaminLindley Vero, il mio esempio è stata una totale assurdità. Sfortunatamente, non riesco a trovare un altro bancomat, qui è troppo tardi.
Daniel Jour,

1
@BarbaraKwarc: !((a == b) && (c == d))e (a != b) || (c != d)sono equivalenti in termini di efficienza di corto circuito.
Oliver Charlesworth

2

Personalizzando il comportamento degli operatori, puoi farli fare quello che vuoi.

Potresti voler personalizzare le cose. Ad esempio, potresti voler personalizzare una classe. Gli oggetti di questa classe possono essere confrontati semplicemente controllando una proprietà specifica. Sapendo che questo è il caso, puoi scrivere un codice specifico che controlla solo le cose minime, invece di controllare ogni singolo bit di ogni singola proprietà nell'intero oggetto.

Immagina un caso in cui puoi capire che qualcosa è diverso altrettanto velocemente, se non più velocemente, di quanto puoi scoprire che qualcosa è lo stesso. Certo, una volta capito se qualcosa è uguale o diverso, allora puoi conoscere il contrario semplicemente girando un po '. Tuttavia, capovolgere quel bit è un'operazione extra. In alcuni casi, quando il codice viene rieseguito molto, il salvataggio di un'operazione (moltiplicato per molte volte) può comportare un aumento complessivo della velocità. (Ad esempio, se si salva un'operazione per pixel di uno schermo megapixel, allora si è appena salvato un milione di operazioni. Moltiplicato per 60 schermi al secondo e si risparmiano ancora più operazioni.)

la risposta di hvd fornisce alcuni esempi aggiuntivi.


2

Sì, perché uno significa "equivalente" e un altro significa "non equivalente" e questi termini si escludono a vicenda. Qualsiasi altro significato per questo operatore è confuso e dovrebbe essere evitato con ogni mezzo.


Non si escludono a vicenda per tutti i casi. Ad esempio, due infiniti entrambi non uguali tra loro e non uguali tra loro.
Vladon,

@vladon può usarne uno anziché l'altro nel caso generico ? No. Questo significa che non sono uguali. Tutto il resto va a una funzione speciale piuttosto che all'operatore == /! =
oliora

@vladon, per favore, invece del caso generico leggi tutti i casi nella mia risposta.
Oliora,

@vladon Per quanto ciò sia vero in matematica, puoi fare un esempio in cui a != bnon è uguale !(a == b)per questo motivo in C?
nitro2k01,

2

Forse una regola incomparabile, dove a != bera falso ed a == bera falso come un bit apolide.

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

Se vuoi riorganizzare i simboli logici, allora! ([A] || [B]) diventa logicamente ([! A] & [! B])
Thijser il

Si noti che il tipo restituito operator==()e operator!=()non sono necessariamente bool, potrebbero essere un enum che includono apolidi se si voleva che eppure gli operatori potrebbero ancora essere definite in modo (a != b) == !(a==b)tiene ..
Lorro
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.