Warum wird Ungleichung in vielen C ++ - Standardbibliothekscodes als (! (A == b)) getestet?

142

Dies ist der Code aus dem C ++ - Standardbibliothekscode remove. Warum wird Ungleichheit als if (!(*first == val))statt getestet if (*first != val)?

 template <class ForwardIterator, class T>
      ForwardIterator remove (ForwardIterator first, ForwardIterator last, const T& val)
 {
     ForwardIterator result = first;
     while (first!=last) {
         if (!(*first == val)) {
             *result = *first;
             ++result;
         }
         ++first;
     }
     return result;
 }
Ahmed Nawar
quelle
2
@BeyelerStudios ist wahrscheinlich richtig. Dies ist auch bei der Implementierung üblich operator!=. Verwenden Sie einfach die operator==Implementierung:bool operator!=(const Foo& other) { return !(*this == other); }
Simon
1
Eigentlich korrigiere ich meine Aussage: Die erwähnten Referenzen entfernen alle Elemente , die gleich dem Wert sind, so dassoperator== erwartet wird, dass sie hier verwendet werden ...
BeyelerStudios
Oh, und es sollte auch ein constBeispiel in meinem vorherigen Kommentar geben, aber Sie verstehen es. (Zu spät, um es zu bearbeiten)
Simon
Der Grund dafür hängt mit einer anderen Frage zusammen (die grundsätzlich mit "Nein, nicht unbedingt" beantwortet werden kann) und dem Konzept, EqualityComparabledas Hurkyl in seiner Antwort erwähnt hat .
Marco13

Antworten:

144

Weil dies bedeutet, dass die einzige Anforderung an T darin besteht, eine zu implementieren operator==. Sie könnten T benötigen, um eine zu haben, operator!=aber die allgemeine Idee hier ist, dass Sie den Benutzer der Vorlage so wenig wie möglich belasten sollten und andere Vorlagen benötigen operator==.

Tom Tanner
quelle
13
Template <Klasse T> Inline-Bool-Operator! = <T a, T b> {return! (a == b); }
Joshua
8
Gibt es ein Szenario, in dem ein Compiler nicht alle Instanzen von =! zu! (==)? Warum ist dies nicht bereits die Standardaktion für den Compiler?
Aidan Gomez
20
@AidanGomez Zum Guten oder Schlechten können Sie die Operatoren überladen, um zu tun, was Sie wollen. Es muss keinen Sinn ergeben oder konsequent sein.
Neil Kirk
10
x != yist nicht definiert als !(x == y). Was ist, wenn diese Operatoren den Analysebaum eines eingebetteten DSL zurückgeben?
Brice M. Dempsey
7
@Joshua Das bricht schlecht, wenn versucht wird, mit SFINAE zu erkennen, ob !=unterstützt wird (würde fälschlicherweise true zurückgeben - auch wenn dies operator==nicht unterstützt wird!). Ich mache mir auch Sorgen, dass dadurch einige Verwendungen von !=mehrdeutig werden.
36

Die meisten Funktionen in STL funktionieren nur mit operator<oder operator==. Dies erfordert, dass der Benutzer nur diese beiden Operatoren (oder manchmal mindestens einen von ihnen) implementiert. Zum Beispiel std::setverwendet operator<(genauer gesagt, std::lesswas operator<standardmäßig aufgerufen wird) und nicht operator>, um die Bestellung zu verwalten. Die removeVorlage in Ihrem Beispiel ist ein ähnlicher Fall - sie wird nur verwendet operator==und nicht, operator!=sodass die operator!=nicht definiert werden muss.

Lukáš Bednařík
quelle
2
Funktionen werden nicht operator<direkt verwendet, sondern stattdessen std::less, was standardmäßig der Fall ist operator<.
Christian Hackl
1
Tatsächlich scheint es, dass Standardalgorithmusfunktionen im Gegensatz zu z. B. std::settatsächlich operator<direkt verwendet werden. Seltsam ...
Christian Hackl
1
Diese Funktionen werden nicht verwendet std::equal_to, sondern operator==wie in der Frage angegeben. Die Situation mit std::lessist ähnlich. Nun, vielleicht std::setist nicht das beste Beispiel.
Lukáš Bednařík
2
@ChristianHackl std::equal_tound std::lesswerden als Standardvorlagenparameter verwendet, wobei der Komparator als Parameter verwendet wird. operator==und operator<werden direkt verwendet, wenn der Typ erforderlich ist, um eine vergleichbare Gleichheit bzw. eine strenge schwache Reihenfolge zu erfüllen, z. B. Iteratoren und Iteratoren mit wahlfreiem Zugriff.
Jan Hudec
28

Dies ist der Code aus der C ++ - Standardbibliothek zum Entfernen von Code.

Falsch. Es ist nicht der C ++ - Standardbibliothekscode remove. Dies ist eine mögliche interne Implementierung der C ++ - Standardbibliotheksfunktion remove. Der C ++ - Standard schreibt keinen tatsächlichen Code vor. Es schreibt Funktionsprototypen und erforderliche Verhaltensweisen vor.

Mit anderen Worten: Aus strenger sprachlicher Sicht existiert der Code, den Sie sehen , nicht . Möglicherweise stammt es aus einer Header-Datei, die mit der Standardbibliotheksimplementierung Ihres Compilers geliefert wird. Beachten Sie, dass der C ++ - Standard nicht einmal erfordert, dass diese Header- Dateien vorhanden sind. Dateien sind nur eine bequeme Möglichkeit für Compiler-Implementierer, die Anforderungen für eine Zeile wie #include <algorithm>(dh das Bereitstellen std::removeund andere verfügbare Funktionen) zu erfüllen .

Warum wird Ungleichheit als if (!(*first == val))statt getestet if (*first != val)?

Weil nur operator==von der Funktion benötigt wird.

Wenn es um das Überladen von Operatoren für benutzerdefinierte Typen geht, können Sie mit der Sprache alle möglichen seltsamen Dinge tun. Sie können sehr gut eine Klasse erstellen, die überladen, operator==aber nicht überladen ist operator!=. Oder noch schlimmer: Sie könnten überladen, operator!=aber völlig unabhängige Dinge tun lassen.

Betrachten Sie dieses Beispiel:

#include <algorithm>
#include <vector>

struct Example
{
    int i;

    Example() : i(0) {}

    bool operator==(Example const& other) const
    {
        return i == other.i;
    }

    bool operator!=(Example const& other) const
    {
        return i == 5; // weird, but nothing stops you
                       // from doing so
    }

};

int main()
{
  std::vector<Example> v(10);
  // ...
  auto it = std::remove(v.begin(), v.end(), Example());
  // ...
}

Bei std::removeVerwendung operator!=wäre das Ergebnis ganz anders.

Christian Hackl
quelle
1
Eine andere zu berücksichtigende Sache ist, dass es für beide möglich sein kann a==bund a!=bfalsch zurückzugeben. Während es möglicherweise nicht immer klar ist, ob eine solche Situation sinnvoller als "gleich" oder "ungleich" angesehen wird, muss eine Funktion, die Gleichheit ausschließlich anhand des Operators "==" definiert, sie als "ungleich" betrachten ", unabhängig davon, welches Verhalten sinnvoller wäre [wenn ich meine Druthers hätte, würde von allen Typen erwartet, dass sich boolesche" == "- und"! = "- Operatoren konsistent verhalten, aber die Tatsache, dass IEEE-754 eine gebrochene Gleichheit vorschreibt Betreiber würden solche Erwartungen erschweren].
Supercat
18
"Aus strenger sprachlicher Sicht existiert der Code, den Sie sehen, nicht." - Wenn ein Standpunkt besagt, dass etwas nicht existiert, Sie das Ding aber tatsächlich betrachten, dann ist der Standpunkt falsch. Tatsächlich sagt der Standard nicht, dass der Code nicht existiert, er sagt nur nicht, dass er existiert :-)
Steve Jessop
@SteveJessop: Sie können den Ausdruck "aus strenger sprachlicher Sicht" durch etwas wie "streng auf Sprachebene" ersetzen. Der Punkt ist, dass der vom OP veröffentlichte Code nicht das Anliegen des Sprachstandards ist.
Christian Hackl
2
@supercat: IEEE-754 erstellt ==und !=verhält sich konsistent, obwohl ich immer gedacht habe, dass alle sechs Beziehungen ausgewertet werden sollten, falsewenn mindestens ein Operand vorhanden ist NaN.
Ben Voigt
@ BenVoigt: Ah, das stimmt. Dadurch verhalten sich die beiden Operatoren in derselben gebrochenen Weise so, dass sie miteinander konsistent sind, aber dennoch alle anderen normalen Axiome verletzen, die mit der Äquivalenz verbunden sind (z. B. halten sie weder a == a noch die Garantie für die Ausführung von Operationen aufrecht bei gleichen Werten ergeben sich gleiche Ergebnisse).
Supercat
15

Einige gute Antworten hier. Ich wollte nur eine kleine Notiz hinzufügen.

Wie alle guten Bibliotheken basiert die Standardbibliothek auf (mindestens) zwei sehr wichtigen Prinzipien:

  1. Übernehmen Sie den Benutzern Ihrer Bibliothek die geringste Verantwortung, mit der Sie durchkommen können. Ein Teil davon hat damit zu tun, dass sie bei der Verwendung Ihrer Benutzeroberfläche den geringsten Arbeitsaufwand haben. (wie das Definieren von so wenigen Operatoren, wie Sie durchkommen können). Der andere Teil hat damit zu tun, dass sie nicht überrascht werden oder dass sie Fehlercodes überprüfen müssen (also die Schnittstellen konsistent halten und Ausnahmen auslösen, <stdexcept>wenn etwas schief geht).

  2. Beseitigen Sie alle logischen Redundanzen . Alle Vergleiche können lediglich abgeleitet operator<werden. Warum also verlangen, dass Benutzer andere definieren? z.B:

    (a> b) ist äquivalent zu (b <a)

    (a> = b) ist äquivalent zu! (a <b)

    (a == b) ist äquivalent zu! ((a <b) || (b <a))

    und so weiter.

    In diesem Sinne könnte man sich natürlich fragen, warum dies unordered_maperforderlich ist operator==(zumindest standardmäßig) und nicht operator<. Die Antwort ist, dass in einer Hash-Tabelle der einzige Vergleich, den wir jemals benötigen, ein Vergleich für die Gleichheit ist. Daher ist es logisch konsistenter (dh für den Bibliotheksbenutzer sinnvoller), von ihm die Definition eines Gleichheitsoperators zu verlangen. Das Erfordernis eines operator<wäre verwirrend, da nicht sofort klar ist, warum Sie es benötigen würden.

Richard Hodges
quelle
10
Zu Ihrem zweiten Punkt: Es gibt Typen, die logisch auf Gleichheit verglichen werden können, auch wenn keine logische Reihenfolge existiert oder für die die Erstellung einer Reihenfolge sehr künstlich wäre. Zum Beispiel ist Rot rot und Rot ist nicht grün, aber ist Rot von Natur aus weniger als grün?
Christian Hackl
1
Stimme voll und ganz zu. Man würde diese Artikel nicht in einem bestellten Container aufbewahren, da es keine logische Reihenfolge gibt. Sie könnten geeigneter in einem ungeordneten Behälter aufbewahrt werden, der operator==(und hash) erfordert .
Richard Hodges
3. Überlasten Sie möglichst wenige Standardoperatoren. Dies ist ein weiterer Grund, den sie implementiert haben !(a==b). Da eine unreflektierte Überladung von Operatoren leicht dazu führen kann, dass das C ++ - Programm völlig durcheinander gerät (und der Programmierer verrückt wird, weil das Debuggen seines Codes zu einer unmöglichen Aufgabe werden kann, da das Auffinden des Täters eines bestimmten Fehlers einer Odyssee ähnelt).
Syntaxfehler
!((a < b) || (b < a))verwendet einen Bool-Operator weniger, daher ist es wahrscheinlich schneller
Filip Haglund
1
In Wirklichkeit tun sie das nicht. In der Assemblersprache werden alle Vergleiche als Subtraktion implementiert, gefolgt von Testübertrag und Null-Bits im Flags-Register. Alles andere ist nur syntaktischer Zucker.
Richard Hodges
8

Das EqualityComparableKonzept erfordert nur die operator==Definition.

Folglich kann sich jede Funktion, die behauptet, mit befriedigenden Typen zu arbeiten, EqualityComparable nicht auf die Existenz von operator!=Objekten dieses Typs verlassen. (es sei denn, es gibt zusätzliche Anforderungen, die das Vorhandensein von implizieren operator!=).


quelle
1

Der vielversprechendste Ansatz besteht darin, eine Methode zu finden, mit der ermittelt werden kann, ob operator == für einen bestimmten Typ aufgerufen werden kann, und diese dann nur zu unterstützen, wenn sie verfügbar ist. In anderen Situationen wird eine Ausnahme ausgelöst. Bisher ist jedoch kein Weg bekannt, um festzustellen, ob ein beliebiger Operatorausdruck f == g geeignet definiert ist. Die beste bekannte Lösung weist die folgenden unerwünschten Eigenschaften auf:

  • Schlägt zur Kompilierungszeit für Objekte fehl, auf die auf operator == nicht zugegriffen werden kann (z. B. weil es privat ist).
  • Schlägt zur Kompilierungszeit fehl, wenn der Aufruf von operator == nicht eindeutig ist.
  • Scheint korrekt zu sein, wenn die Deklaration operator == korrekt ist, obwohl operator == möglicherweise nicht kompiliert werden kann.

Von Boost FAQ: Quelle

Da Sie wissen, dass das Erfordernis der ==Implementierung eine Belastung darstellt , möchten Sie niemals eine zusätzliche Belastung schaffen, indem Sie auch die !=Implementierung erfordern .

Für mich persönlich geht es um SOLID (objektorientiertes Design) L-Teil - Liskov-Substitutionsprinzip: „Objekte in einem Programm sollten durch Instanzen ihrer Subtypen ersetzt werden können, ohne die Richtigkeit dieses Programms zu ändern.“ In diesem Fall ist es der Operator ! = , Den ich in der booleschen Logik durch == und boolean inverse ersetzen kann .

Margus
quelle