Perché i compilatori C ++ non definiscono operator == e operator! =?


302

Sono un grande fan di lasciare che il compilatore faccia il maggior lavoro possibile per te. Quando si scrive una classe semplice, il compilatore può fornire quanto segue come "gratuito":

  • Un costruttore predefinito (vuoto)
  • Un costruttore di copie
  • Un distruttore
  • Un operatore di assegnazione ( operator=)

Ma non può darti alcun operatore di confronto - come operator==o operator!=. Per esempio:

class foo
{
public:
    std::string str_;
    int n_;
};

foo f1;        // Works
foo f2(f1);    // Works
foo f3;
f3 = f2;       // Works

if (f3 == f2)  // Fails
{ }

if (f3 != f2)  // Fails
{ }

C'è una buona ragione per questo? Perché eseguire un confronto tra i membri sarebbe un problema? Ovviamente se la classe alloca memoria allora vorresti stare attento, ma per una semplice classe sicuramente il compilatore potrebbe fare questo per te?


4
Naturalmente, anche il distruttore è fornito gratuitamente.
Johann Gerell,

23
In uno dei suoi recenti discorsi, Alex Stepanov ha sottolineato che è stato un errore non avere un predefinito automatico ==, allo stesso modo in cui esiste un compito automatico predefinito ( =) in determinate condizioni. (L'argomento sui puntatori è incoerente perché la logica si applica sia per =e ==, e non solo per il secondo).
alfC

2
@becko È uno della serie di A9: youtube.com/watch?v=k-meLQaYP5Y , non ricordo in quale dei colloqui. C'è anche una proposta che sembra stia arrivando al C ++ 17 open-std.org/JTC1/SC22/WG21/docs/papers/2016/p0221r0.html
alfC

1
@becko, è uno dei primi nella serie "Programmazione efficiente con componenti" o "Programmazione di conversazioni" sia su A9, disponibile su Youtube.
alfC

1
@becko In realtà c'è una risposta al di sotto che punta al punto di vista di Alex stackoverflow.com/a/23329089/225186
ALFC

Risposte:


71

Il compilatore non saprebbe se si desidera un confronto puntatore o un confronto approfondito (interno).

È più sicuro non implementarlo e lasciare che il programmatore lo faccia da solo. Quindi possono fare tutte le ipotesi che vogliono.


293
Questo problema non gli impedisce di generare un ctor copia, dove è abbastanza dannoso.
MSalters,

78
I costruttori di copie (e operator=) generalmente lavorano nello stesso contesto degli operatori di confronto, ovvero c'è un'aspettativa che dopo l'esecuzione a = b, a == bè vera. Ha sicuramente senso per il compilatore fornire un valore predefinito operator==usando la stessa semantica di valore aggregato per cui lo fa operator=. Ho il sospetto che Paercebal sia effettivamente corretto qui in quanto operator=(e copia ctor) sono forniti esclusivamente per la compatibilità C, e non volevano peggiorare la situazione.
Pavel Minaev,

46
-1. Ovviamente vuoi un confronto approfondito, se il programmatore voleva un confronto puntatore, scriverebbe (& f1 == & f2)
Viktor Sehr,

62
Viktor, ti suggerisco di ripensare la tua risposta. Se la classe Foo contiene una barra *, come farebbe il compilatore a sapere se Foo :: operator == vuole confrontare l'indirizzo di Bar * o il contenuto di Bar?
Mark Ingram,

46
@Mark: se contiene un puntatore, il confronto dei valori del puntatore è ragionevole - se contiene un valore, il confronto dei valori è ragionevole. In circostanze eccezionali, il programmatore potrebbe ignorare. Questo è proprio come il linguaggio implementa il confronto tra ints e pointer-to-ints.
Eamon Nerbonne

317

L'argomento secondo cui se il compilatore può fornire un costruttore di copie predefinito, dovrebbe essere in grado di fornire un valore predefinito simile operator==()ha un certo senso. Penso che il motivo della decisione di non fornire un valore predefinito generato dal compilatore per questo operatore possa essere intuito da ciò che Stroustrup ha detto a proposito del costruttore di copie predefinito in "The Design and Evolution of C ++" (Sezione 11.4.1 - Controllo della copia) :

Personalmente ritengo sfortunato che le operazioni di copia siano definite per impostazione predefinita e proibisco la copia di oggetti di molte delle mie classi. Tuttavia, C ++ ha ereditato la sua assegnazione predefinita e copia i costruttori da C, e vengono spesso utilizzati.

Quindi, invece di "perché C ++ non ha un valore predefinito operator==()?", La domanda avrebbe dovuto essere "perché C ++ ha un compito predefinito e un costruttore di copie?", Con la risposta che tali elementi sono stati inclusi con riluttanza da Stroustrup per la retrocompatibilità con C (probabilmente la causa della maggior parte delle verruche del C ++, ma probabilmente anche il motivo principale della popolarità del C ++).

Per i miei scopi, nel mio IDE lo snippet che utilizzo per le nuove classi contiene dichiarazioni per un operatore di assegnazione privato e costruttore di copia in modo che quando creo una nuova classe non ottengo operazioni di assegnazione e copia predefinite, devo rimuovere esplicitamente la dichiarazione di tali operazioni dal private: sezione se voglio che il compilatore sia in grado di generarle per me.


29
Buona risposta. Vorrei solo sottolineare che in C ++ 11, anziché rendere privati ​​l'operatore di assegnazione e il costruttore di copie, è possibile rimuoverli completamente in questo modo: Foo(const Foo&) = delete; // no copy constructoreFoo& Foo=(const Foo&) = delete; // no assignment operator
karadoc,

9
"Tuttavia, C ++ ha ereditato la sua assegnazione predefinita e copia i costruttori da C" Ciò non implica il motivo per cui devi creare TUTTI i tipi di C ++ in questo modo. Avrebbero dovuto limitarlo a semplici vecchi POD, solo i tipi che sono già in C, non di più.
thesaint

3
Posso certamente capire perché il C ++ ha ereditato questi comportamenti struct, ma desidero che si classcomporti in modo diverso (e sano). Nel processo, avrebbe anche dato una differenza più significativa tra structe classaccanto all'accesso predefinito.
jamesdlin,

@jamesdlin Se si desidera una regola, disabilitare la dichiarazione e la definizione implicite di ctors e assegnazione se viene dichiarato un dtor avrebbe più senso.
Deduplicatore

1
Non vedo ancora alcun danno nel lasciare che il programmatore ordini esplicitamente al compilatore di creare un operator==. A questo punto è solo zucchero di sintassi per un po 'di codice sulla piastra della caldaia. Se hai paura che in questo modo il programmatore possa trascurare alcuni puntatori tra i campi di classe, puoi aggiungere una condizione che possa funzionare solo su tipi e oggetti primitivi che hanno operatori di uguaglianza stessi. Non vi è alcun motivo per impedirlo del tutto, però.
NO_NAME

93

Anche in C ++ 20, il compilatore non genererà ancora implicitamente operator==per te

struct foo
{
    std::string str;
    int n;
};

assert(foo{"Anton", 1} == foo{"Anton", 1}); // ill-formed

Ma otterrai la possibilità di eseguire il default in modo esplicito == dal C ++ 20 :

struct foo
{
    std::string str;
    int n;

    // either member form
    bool operator==(foo const&) const = default;
    // ... or friend form
    friend bool operator==(foo const&, foo const&) = default;
};

L' impostazione predefinita ==fa il membro ==(nello stesso modo in cui il costruttore di copie predefinito fa la costruzione della copia del membro). Le nuove regole forniscono anche la relazione prevista tra ==e !=. Ad esempio, con la dichiarazione sopra, posso scrivere entrambi:

assert(foo{"Anton", 1} == foo{"Anton", 1}); // ok!
assert(foo{"Anton", 1} != foo{"Anton", 2}); // ok!

Questa caratteristica specifica (default operator==e simmetria tra ==e !=) proviene da una proposta che faceva parte della più ampia funzionalità linguistica che è operator<=>.


Sai se ci sono aggiornamenti più recenti su questo? Sarà disponibile in c ++ 17?
dcmm88,

3
@ dcmm88 Purtroppo non sarà disponibile in C ++ 17. Ho aggiornato la risposta.
Anton Savin,

2
Una proposta modificata che consente la stessa cosa (tranne la forma abbreviata) sarà in C ++ 20 però :)
Rakete1111

Quindi in pratica devi specificare = default, per cosa che non è stata creata di default, giusto? Mi sembra ossimoro ("default esplicito").
artin

@artin Ha senso come l'aggiunta di nuove funzionalità al linguaggio non dovrebbe interrompere l'implementazione esistente. L'aggiunta di nuovi standard di libreria o nuove cose che il compilatore può fare è una cosa. L'aggiunta di nuove funzioni membro in cui non esistevano in precedenza è una storia completamente diversa. Per proteggere il tuo progetto da errori richiederebbe molto più sforzo. Preferirei personalmente il flag del compilatore per alternare tra default esplicito e implicito. Compilate un progetto dallo standard C ++ precedente, utilizzate il default esplicito dal flag del compilatore. Hai già aggiornato il compilatore, quindi dovresti configurarlo correttamente. Per i nuovi progetti lo rendono implicito.
Maciej Załucki,

44

IMHO, non esiste una "buona" ragione. Il motivo per cui ci sono così tante persone che concordano con questa decisione progettuale è perché non hanno imparato a dominare il potere della semantica basata sul valore. Le persone devono scrivere molti costruttori di copie personalizzate, operatori di confronto e distruttori perché usano puntatori non elaborati nella loro implementazione.

Quando si utilizzano i puntatori intelligenti appropriati (come std :: shared_ptr), il costruttore di copie predefinito di solito va bene e l'ovvia implementazione dell'ipotetico operatore di confronto predefinito andrebbe bene.


39

Ha risposto che C ++ non ha fatto == perché C no, ed ecco perché C fornisce solo default = ma no == al primo posto. C voleva mantenerlo semplice: C implementato = da memcpy; tuttavia, == non può essere implementato da memcmp a causa del riempimento. Poiché il padding non è inizializzato, memcmp afferma che sono diversi anche se sono uguali. Lo stesso problema esiste per la classe vuota: memcmp dice che sono diversi perché la dimensione delle classi vuote non è zero. Si può vedere dall'alto che l'implementazione == è più complicata dell'implementazione = in C. Alcuni esempi di codice al riguardo. La tua correzione è apprezzata se sbaglio.


6
C ++ non usa memcpy per operator=- funzionerebbe solo per i tipi POD, ma C ++ fornisce un valore predefinito anche operator=per i tipi non POD.
Flexo

2
Sì, il C ++ implementato = in un modo più sofisticato. Sembra che C abbia appena implementato = con un semplice memcpy.
Rio Wing,

Il contenuto di questa risposta dovrebbe essere messo insieme a quello di Michael. Corregge la domanda, quindi risponde.
Sgene9,

27

In questo video Alex Stepanov, il creatore di STL affronta questa domanda alle 13:00 circa. Riassumendo, avendo osservato l'evoluzione del C ++, sostiene che:

  • È un peccato che == e! = Non siano dichiarati implicitamente (e Bjarne è d'accordo con lui). Un linguaggio corretto dovrebbe avere quelle cose pronte per te (prosegue suggerendo che non dovresti essere in grado di definire un ! = Che rompe la semantica di == )
  • Il motivo per cui questo è il caso ha le sue radici (come molti dei problemi di C ++) in C. Lì, l'operatore di assegnazione è implicitamente definito con assegnazione bit per bit ma ciò non funzionerebbe per == . Una spiegazione più dettagliata può essere trovata in questo articolo di Bjarne Stroustrup.
  • Nella domanda di follow-up Perché allora non è stato usato un membro per confronto dei membri , dice una cosa incredibile : C era una specie di linguaggio casalingo e il ragazzo che implementava queste cose per Ritchie gli disse che trovava difficile implementarlo!

Quindi dice che in un futuro (distante) == e ! = Verranno generati implicitamente.


2
sembra che questo futuro lontano non sarà il 2017, né il 18, né il 19, beh, cogli la mia deriva ...
UmNyobe,

18

C ++ 20 fornisce un modo per implementare facilmente un operatore di confronto predefinito.

Esempio da cppreference.com :

class Point {
    int x;
    int y;
public:
    auto operator<=>(const Point&) const = default;
    // ... non-comparison functions ...
};

// compiler implicitly declares operator== and all four relational operators work
Point pt1, pt2;
if (pt1 == pt2) { /*...*/ } // ok, calls implicit Point::operator==
std::set<Point> s; // ok
s.insert(pt1); // ok
if (pt1 <= pt2) { /*...*/ } // ok, makes only a single call to Point::operator<=>

4
Sono sorpreso che abbiano usato Pointun esempio per un'operazione di ordinazione , dal momento che non esiste un modo predefinito ragionevole per ordinare due punti con xe ycoordinate ...
pipe

4
@pipe Se non ti interessa l'ordine in cui sono gli elementi, ha senso usare l'operatore predefinito. Ad esempio, è possibile utilizzare std::setper assicurarsi che tutti i punti siano univoci e vengano std::setutilizzati operator<solo.
VII

Informazioni sul tipo di reso auto: in questo caso possiamo sempre presumere che provenga std::strong_orderingda #include <compare>?
kevinarpe,

1
@kevinarpe Il tipo restituito è std::common_comparison_category_t, che per questa classe diventa l'ordinamento predefinito ( std::strong_ordering).
VII

15

Non è possibile definire il valore predefinito ==, ma è possibile definire il valore predefinito !=tramite il ==quale di solito è necessario definire voi stessi. Per questo dovresti fare le seguenti cose:

#include <utility>
using namespace std::rel_ops;
...

class FooClass
{
public:
  bool operator== (const FooClass& other) const {
  // ...
  }
};

Puoi vedere http://www.cplusplus.com/reference/std/utility/rel_ops/ per i dettagli.

Inoltre, se definito operator< , gli operatori per <=,>,> = possono essere dedotti da esso durante l'utilizzo std::rel_ops.

Ma dovresti stare attento quando lo usi std::rel_opsperché gli operatori di confronto possono essere dedotti per i tipi che non ti aspetti.

È preferibile utilizzare un modo più preferito di dedurre l'operatore correlato da quello base boost :: operatori .

L'approccio utilizzato in boost è migliore perché definisce l'utilizzo dell'operatore per la classe desiderata, non per tutte le classi nell'ambito.

Puoi anche generare "+" da "+ =", - da "- =", ecc ... (vedi l'elenco completo qui )


Non ho ricevuto il valore predefinito !=dopo aver scritto l' ==operatore. O l'ho fatto, ma era carente const. Ho dovuto scriverlo anch'io e tutto è andato bene.
Giovanni,

puoi giocare con costanza per ottenere i risultati necessari. Senza codice è difficile dire cosa c'è che non va.
sergtk,

2
C'è un motivo che è rel_opsstato deprecato in C ++ 20: perché non funziona , almeno non ovunque, e certamente non in modo coerente. Non esiste un modo affidabile per arrivare sort_decreasing()alla compilazione. D'altra parte, Boost.Operators funziona e ha sempre funzionato.
Barry,

10

C ++ 0x ha avuto una proposta per funzioni predefinite, quindi potresti dire default operator==; che abbiamo imparato che aiuta a rendere esplicite queste cose.


3
Pensavo che solo le "funzioni speciali del membro" (costruttore predefinito, costruttore di copia, operatore di assegnazione e distruttore) potessero essere esplicitamente predefinite. Lo hanno esteso ad altri operatori?
Michael Burr,

4
Spostare costruttore può anche essere predefinito, ma non credo che questo si applichi operator==. È un peccato.
Pavel Minaev,

5

Concettualmente non è facile definire l'uguaglianza. Anche per i dati POD, si potrebbe sostenere che anche se i campi sono uguali, ma è un oggetto diverso (a un indirizzo diverso) non è necessariamente uguale. Questo in realtà dipende dall'uso dell'operatore. Sfortunatamente il tuo compilatore non è sensitivo e non può dedurlo.

Oltre a ciò, le funzioni predefinite sono ottimi modi per spararsi al piede. Le impostazioni predefinite che descrivi sono sostanzialmente lì per mantenere la compatibilità con le strutture POD. Tuttavia, causano il caos più che sufficiente con gli sviluppatori che se ne dimenticano o la semantica delle implementazioni predefinite.


10
Non vi è alcuna ambiguità per le strutture POD: dovrebbero comportarsi esattamente allo stesso modo di qualsiasi altro tipo di POD, che è l'uguaglianza dei valori (piuttosto che l'uguaglianza di riferimento). Uno intcreato tramite copia ctor da un altro è uguale a quello da cui è stato creato; l'unica cosa logica da fare per uno structdei due intcampi è lavorare esattamente allo stesso modo.
Pavel Minaev,

1
@mgiuca: vedo una notevole utilità per una relazione di equivalenza universale che consentirebbe a qualsiasi tipo che si comporta come un valore di essere usato come chiave in un dizionario o in una raccolta simile. Tuttavia, tali raccolte non possono comportarsi utilmente senza una relazione di equivalenza garantita-riflessiva. IMHO, la soluzione migliore sarebbe quella di definire un nuovo operatore che tutti i tipi predefiniti potrebbero implementare in modo sensato e definire alcuni nuovi tipi di puntatori che erano come quelli esistenti, tranne per il fatto che alcuni definirebbero l'uguaglianza come equivalenza di riferimento mentre altri si concatenerebbero al bersaglio operatore di equivalenza.
supercat

1
@supercat Per analogia, potresti fare quasi lo stesso argomento per l' +operatore in quanto non associativo per i float; cioè (x + y) + z! = x + (y + z), a causa del modo in cui si verifica l'arrotondamento FP. (Probabilmente, questo è un problema molto peggiore rispetto al ==fatto che è vero per i valori numerici normali.) Potresti suggerire di aggiungere un nuovo operatore addizione che funziona per tutti i tipi numerici (anche int) ed è quasi lo stesso di +ma è associativo ( in qualche modo). Ma poi aggiungeresti gonfiore e confusione alla lingua senza aiutare davvero tante persone.
mgiuca,

1
@mgiuca: Avere cose che sono abbastanza simili tranne che nei casi limite è spesso estremamente utile, e gli sforzi sbagliati per evitare tali cose si traducono in una complessità molto inutile. Se a volte il codice client richiederà la gestione dei casi limite in un modo e talvolta ne dovrà essere gestito un altro, avere un metodo per ogni stile di gestione eliminerà un sacco di codice di gestione dei casi limite nel client. Per quanto riguarda la tua analogia, non c'è modo di definire operazioni su valori a virgola mobile di dimensioni fisse per produrre risultati transitivi in ​​tutti i casi (anche se alcune lingue degli anni '80 avevano una semantica migliore ...
supercat

1
... rispetto a oggi in questo senso) e quindi il fatto che non facciano l'impossibile non dovrebbe essere una sorpresa. Non vi è tuttavia alcun ostacolo fondamentale all'attuazione di una relazione di equivalenza che sia universalmente applicabile a qualsiasi tipo di valore che può essere copiato.
supercat

1

C'è una buona ragione per questo? Perché eseguire un confronto tra i membri sarebbe un problema?

Potrebbe non essere un problema dal punto di vista funzionale, ma in termini di prestazioni, il confronto predefinito membro per membro può essere più subottimale rispetto all'assegnazione / copia dei membri predefiniti. Diversamente dall'ordine di assegnazione, l'ordine di confronto influisce sulle prestazioni poiché il primo membro disuguale implica che il resto può essere ignorato. Quindi, se ci sono alcuni membri che di solito sono uguali, si desidera confrontarli per ultimi e il compilatore non sa quali membri hanno più probabilità di essere uguali.

Considera questo esempio, in cui verboseDescriptionè selezionata una stringa lunga da una serie relativamente piccola di possibili descrizioni meteorologiche.

class LocalWeatherRecord {
    std::string verboseDescription;
    std::tm date;
    bool operator==(const LocalWeatherRecord& other){
        return date==other.date
            && verboseDescription==other.verboseDescription;
    // The above makes a lot more sense than
     // return verboseDescription==other.verboseDescription
     //     && date==other.date;
    // because some verboseDescriptions are liable to be same/similar
    }
}

(Ovviamente il compilatore avrebbe il diritto di ignorare l'ordine dei confronti se riconosce che non hanno effetti collaterali, ma presumibilmente prenderebbe comunque la sua coda dal codice sorgente dove non ha migliori informazioni.)


Ma nessuno ti impedisce di scrivere un confronto ottimizzato definito dall'utente se trovi un problema di prestazioni. Nella mia esperienza, però, sarebbe una minuscola minoranza di casi.
Peter - Ripristina Monica il

1

Solo così le risposte a questa domanda rimangono complete col passare del tempo: dal C ++ 20 può essere generato automaticamente con comando auto operator<=>(const foo&) const = default;

Genererà tutti gli operatori: ==,! =, <, <=,> E> =, vedi https://en.cppreference.com/w/cpp/language/default_comparisons per i dettagli.

A causa dell'aspetto dell'operatore <=>, si chiama operatore dell'astronave. Vedi anche Perché è necessario l'operatore navicella spaziale <=> in C ++? .

EDIT: anche in C ++ 11 un sostituto abbastanza pulito per questo è disponibile con std::tievedere https://en.cppreference.com/w/cpp/utility/tuple/tie per un esempio di codice completo con bool operator<(…). La parte interessante, modificata per funzionare ==è:

#include <tuple>

struct S {
………
bool operator==(const S& rhs) const
    {
        // compares n to rhs.n,
        // then s to rhs.s,
        // then d to rhs.d
        return std::tie(n, s, d) == std::tie(rhs.n, rhs.s, rhs.d);
    }
};

std::tie funziona con tutti gli operatori di confronto ed è completamente ottimizzato dal compilatore.


-1

Sono d'accordo, per le classi di tipo POD il compilatore potrebbe farlo per te. Tuttavia, ciò che potresti considerare semplice potrebbe essere sbagliato nel compilatore. Quindi è meglio lasciare che il programmatore lo faccia.

Una volta ho avuto un caso POD in cui due dei campi erano unici, quindi un confronto non sarebbe mai stato considerato vero. Tuttavia, il confronto di cui avevo bisogno solo rispetto al payload - qualcosa che il compilatore non avrebbe mai capito o avrebbe mai potuto capire da solo.

Inoltre - non ci mettono molto a scrivere, vero ?!

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.