wake-up-neo.com

Sind == und! = Voneinander abhängig?

Ich lerne etwas über das Überladen von Operatoren in C++ und sehe, dass == Und != Nur einige spezielle Funktionen sind, die für benutzerdefinierte Typen angepasst werden können. Mein Anliegen ist jedoch, warum es zwei separate Definitionen benötigt? Ich dachte, wenn a == b Wahr ist, dann ist a != b Automatisch falsch und umgekehrt, und es gibt keine andere Möglichkeit, da a != b Per Definition !(a == b). Und ich konnte mir keine Situation vorstellen, in der das nicht stimmte. Aber vielleicht ist meine Vorstellungskraft begrenzt oder ich kenne etwas nicht?

Ich weiß, dass ich eines in Bezug auf das andere definieren kann, aber das ist nicht das, wonach ich frage. Ich frage auch nicht nach der Unterscheidung zwischen dem Vergleichen von Objekten nach Wert oder nach Identität. Oder ob zwei Objekte gleichzeitig gleich und ungleich sein können (dies ist definitiv keine Option! Diese Dinge schließen sich gegenseitig aus). Was ich frage, ist das:

Gibt es eine mögliche Situation, in der das Stellen von Fragen zu zwei gleichen Objekten Sinn macht, das Stellen von Fragen zu ihnen aber nicht Sinn macht, wenn sie gleich sind? (entweder aus Sicht des Benutzers oder aus Sicht des Implementierers)

Wenn es keine solche Möglichkeit gibt, warum in C++ werden dann diese beiden Operatoren als zwei unterschiedliche Funktionen definiert?

291
BarbaraKwarc

Sie möchten nicht, dass die Sprache a != b Automatisch als !(a == b) umschreibt, wenn a == b Etwas anderes als bool zurückgibt. Und dafür gibt es einige Gründe.

Möglicherweise haben Sie Ausdrucks-Builder-Objekte, bei denen a == b Keinen Vergleich vornimmt und nicht dazu gedacht ist, sondern lediglich einen Ausdrucksknoten erstellt, der a == b Darstellt.

Möglicherweise haben Sie eine verzögerte Auswertung, bei der a == b Keinen direkten Vergleich vornimmt, sondern eine Art lazy<bool> Zurückgibt, die in bool konvertiert werden kann. implizit oder explizit zu einem späteren Zeitpunkt, um den Vergleich tatsächlich durchzuführen. Möglicherweise kombiniert mit den Ausdrucks-Builder-Objekten, um eine vollständige Ausdrucksoptimierung vor der Auswertung zu ermöglichen.

Möglicherweise haben Sie eine benutzerdefinierte Vorlagenklasse optional<T>, In der Sie bei Angabe der optionalen Variablen t und ut == u Zulassen möchten, aber optional<bool>.

Es gibt wahrscheinlich mehr, an das ich nicht gedacht habe. Und obwohl in diesen Beispielen die Operationen a == b Und a != b Beide sinnvoll sind, ist a != b Immer noch nicht dasselbe wie !(a == b), also getrennt Definitionen sind erforderlich.

273
user743382

Wenn es keine solche Möglichkeit gibt, warum in C++ werden dann diese beiden Operatoren als zwei unterschiedliche Funktionen definiert?

Weil Sie sie überladen können, und indem Sie sie überladen, können Sie ihnen eine völlig andere Bedeutung geben als ihrer ursprünglichen.

Nehmen wir zum Beispiel den Operator <<, Ursprünglich der bitweise Linksverschiebungsoperator, der jetzt allgemein als Einfügeoperator überladen ist, wie in std::cout << something; völlig andere Bedeutung als das Original.

Wenn Sie also akzeptieren, dass sich die Bedeutung eines Operators ändert, wenn Sie ihn überladen, dann gibt es keinen Grund, den Benutzer daran zu hindern, dem Operator == Eine Bedeutung zuzuweisen, die nicht genau die Negation ist des Operators !=, obwohl dies verwirrend sein könnte.

111
shrike

Mein Anliegen ist jedoch, warum zwei separate Definitionen erforderlich sind.

Sie müssen nicht beide definieren.
Wenn sie sich gegenseitig ausschließen, können Sie sich dennoch kurz fassen, indem Sie nur == und < neben std :: rel_ops

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

Gibt es eine mögliche Situation, in der das Stellen von Fragen, wonach zwei Objekte gleich sind, Sinn macht, das Stellen von Fragen, die nicht gleich sind, aber Sinn macht?

Wir verbinden diese Operatoren oft mit Gleichheit.
Obwohl sie sich auf diese Weise bei grundlegenden Datentypen verhalten, besteht keine Verpflichtung, dass dies bei benutzerdefinierten Datentypen der Fall ist. Sie müssen nicht einmal einen Narren zurückgeben, wenn Sie nicht möchten.

Ich habe Leute gesehen, die Operatoren auf bizarre Weise überlasteten, nur um herauszufinden, dass dies für ihre domänenspezifische Anwendung Sinn macht. Selbst wenn die Benutzeroberfläche anzeigt, dass sie sich gegenseitig ausschließen, möchte der Autor möglicherweise eine bestimmte interne Logik hinzufügen.

(entweder aus Sicht des Benutzers oder aus Sicht des Implementierers)

Ich weiß, dass Sie ein bestimmtes Beispiel wollen,
Also hier ist einer aus dem Catch Testing Framework , den ich für praktisch hielt:

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

Diese Operatoren machen verschiedene Dinge, und es wäre nicht sinnvoll, eine Methode als! (Nicht) der anderen zu definieren. Der Grund dafür ist, dass das Framework den durchgeführten Vergleich ausdrucken kann. Dazu muss der Kontext des überladenen Operators erfasst werden.

60
Trevor Hickey

Es gibt einige sehr gut etablierte Konventionen, in denen (a == b) und (a != b) sind beide falsch nicht unbedingt Gegensätze. Insbesondere in SQL ergibt jeder Vergleich mit NULL NULL, nicht wahr oder falsch.

Es ist wahrscheinlich keine gute Idee, neue Beispiele dafür zu erstellen, wenn dies überhaupt möglich ist, da dies so unintuitiv ist. Wenn Sie jedoch versuchen, eine vorhandene Konvention zu modellieren, ist es hilfreich, die Option zu haben, dass sich Ihre Operatoren "richtig" verhalten Kontext.

42
Jander

Ich werde nur den zweiten Teil Ihrer Frage beantworten, nämlich:

Wenn es keine solche Möglichkeit gibt, warum in C++ werden dann diese beiden Operatoren als zwei unterschiedliche Funktionen definiert?

Ein Grund, warum es sinnvoll ist, dem Entwickler zu erlauben, beide zu überlasten, ist die Leistung. Sie können Optimierungen zulassen, indem Sie sowohl == Als auch != Implementieren. Dann könnte x != y Billiger sein als !(x == y). Einige Compiler sind möglicherweise in der Lage, es für Sie zu optimieren, möglicherweise jedoch nicht, insbesondere wenn Sie komplexe Objekte mit vielen Verzweigungen haben.

Selbst in Haskell, wo Entwickler Gesetze und mathematische Konzepte sehr ernst nehmen, darf man sowohl == Als auch /= Überladen, wie Sie hier sehen können ( 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'

Dies würde wahrscheinlich als Mikrooptimierung angesehen, könnte jedoch in einigen Fällen gerechtfertigt sein.

23
Centril

Gibt es eine mögliche Situation, in der das Stellen von Fragen, wonach zwei Objekte gleich sind, Sinn macht, aber das Stellen von Fragen, wonach sie nicht gleich sind, keinen Sinn macht? (entweder aus Sicht des Benutzers oder aus Sicht des Implementierers)

Das ist eine Meinung. Vielleicht nicht. Da die Sprachdesigner jedoch nicht allwissend waren, beschlossen sie nicht, Menschen einzuschränken, die möglicherweise Situationen finden, in denen dies sinnvoll sein könnte (zumindest für sie).

16

In Reaktion auf die Bearbeitung;

Das heißt, wenn es für einen Typ möglich ist, den Operator == aber nicht das != oder umgekehrt, und wann ist das sinnvoll?.

In general, nein, das ergibt keinen Sinn. Gleichheits- und Vergleichsoperatoren kommen im Allgemeinen in Mengen vor. Wenn es die Gleichheit gibt, dann auch die Ungleichheit; kleiner als, dann größer als und so weiter mit dem <= usw. Ein ähnlicher Ansatz wird auch für die arithmetischen Operatoren angewendet, sie kommen im Allgemeinen auch in natürlichen logischen Mengen vor.

Dies zeigt sich in der std::rel_ops Namespace. Wenn Sie die Operatoren equality und less than implementieren, erhalten Sie mit diesem Namespace die anderen Operatoren, die entsprechend Ihren ursprünglich implementierten Operatoren implementiert wurden.

Das heißt, gibt es Bedingungen oder Situationen, in denen das eine nicht sofort das andere bedeuten würde oder in Bezug auf die anderen nicht umgesetzt werden könnte? Ja, es gibt wohl nur wenige, aber sie sind da; wieder, wie im rel_ops ein eigener Namespace sein. Aus diesem Grund können Sie die Sprache nutzen, um die von Ihnen gewünschte oder benötigte Semantik auf eine Weise zu erhalten, die für den Benutzer oder Client des Codes immer noch natürlich und intuitiv ist.

Die bereits erwähnte faule Bewertung ist ein hervorragendes Beispiel dafür. Ein weiteres gutes Beispiel ist, ihnen Semantik zu geben, die überhaupt keine Gleichheit oder Ungleichheit bedeutet. Ein ähnliches Beispiel sind die Bitverschiebungsoperatoren << und >> wird zum Einfügen und Extrahieren von Streams verwendet. Obwohl es in allgemeinen Kreisen verpönt sein mag, kann es in einigen domänenspezifischen Bereichen sinnvoll sein.

13
Niall

Wenn der == und != -Operatoren implizieren nicht die Gleichheit, genauso wie die << und >> Stream-Operatoren implizieren keine Bitverschiebung. Wenn Sie die Symbole so behandeln, als ob sie ein anderes Konzept bedeuten, müssen sie sich nicht gegenseitig ausschließen.

Im Sinne der Gleichheit kann es sinnvoll sein, wenn Ihr Anwendungsfall die Behandlung von Objekten als nicht vergleichbar rechtfertigt, sodass jeder Vergleich false (oder einen nicht vergleichbaren Ergebnistyp, wenn Ihre Operatoren non-bool zurückgeben) zurückgibt. Ich kann mir keine spezifische Situation vorstellen, in der dies gerechtfertigt wäre, aber ich konnte mir vorstellen, dass dies vernünftig genug ist.

12
Taywee

Mit großer Kraft kommt große Verantwortung, oder zumindest wirklich gute Styleguides.

== Und != Können überladen werden, um alles zu tun, was Sie wollen. Es ist sowohl ein Segen als auch ein Fluch. Es gibt keine Garantie, dass !=!(a==b) bedeutet.

7
It'sPete
enum BoolPlus {
    kFalse = 0,
    kTrue = 1,
    kFileNotFound = -1
}

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

Ich kann diese Überladung des Operators nicht rechtfertigen, aber im obigen Beispiel ist es unmöglich, operator!= Als das "Gegenteil" von operator== Zu definieren.

6
Dafang Cao

Am Ende überprüfen Sie mit diesen Operatoren, dass der Ausdruck a == b oder a != b gibt einen Booleschen Wert zurück (true oder false). Dieser Ausdruck gibt nach dem Vergleich einen Booleschen Wert zurück, anstatt sich gegenseitig auszuschließen.

5
Anirudh Sohil

[..] warum werden zwei separate Definitionen benötigt?

Eine zu berücksichtigende Sache ist, dass es die Möglichkeit geben könnte, einen dieser Operatoren effizienter zu implementieren, als nur die Negation des anderen zu verwenden.

(Mein Beispiel hier war Müll, aber der springende Punkt ist immer noch, denken Sie an Bloom-Filter, zum Beispiel: Sie ermöglichen ein schnelles Testen, ob etwas nicht in einer Menge ist, aber das Testen, ob es in ist, kann viel mehr dauern Zeit.)

[..] per definitionem ist a != b!(a == b).

Und es liegt in Ihrer Verantwortung als Programmierer, das in den Griff zu bekommen. Wahrscheinlich eine gute Sache, für die man einen Test schreiben kann.

4
Daniel Jour

Vielleicht eine unvergleichliche Regel, in der a != b war falsch und a == b war false wie ein zustandsloses Bit.

if( !(a == b || a != b) ){
    // Stateless
}
2
ToñitoG

Ja, weil eines "gleichwertig" und ein anderes "nicht gleichwertig" bedeutet und sich diese Begriffe gegenseitig ausschließen. Jede andere Bedeutung für diese Operatoren ist verwirrend und sollte unter allen Umständen vermieden werden.

2
oliora

Indem Sie das Verhalten der Operatoren anpassen, können Sie sie dazu bringen, das zu tun, was Sie wollen.

Vielleicht möchten Sie die Dinge anpassen. Beispielsweise möchten Sie möglicherweise eine Klasse anpassen. Objekte dieser Klasse können durch einfaches Überprüfen einer bestimmten Eigenschaft verglichen werden. Wenn Sie wissen, dass dies der Fall ist, können Sie einen bestimmten Code schreiben, der nur die Mindestanforderungen überprüft, anstatt jedes einzelne Bit jeder einzelnen Eigenschaft im gesamten Objekt zu überprüfen.

Stellen Sie sich einen Fall vor, in dem Sie herausfinden können, dass etwas anders ist, genauso schnell, wenn nicht sogar schneller, als Sie herausfinden können, dass etwas dasselbe ist. Zugegeben, wenn Sie herausgefunden haben, ob etwas gleich oder verschieden ist, können Sie das Gegenteil einfach durch leichtes Umdrehen erkennen. Das Umdrehen dieses Bits ist jedoch eine zusätzliche Operation. In einigen Fällen kann das Speichern einer Operation (multipliziert mit dem Vielfachen) zu einer allgemeinen Geschwindigkeitssteigerung führen, wenn Code häufig erneut ausgeführt wird. (Wenn Sie beispielsweise einen Vorgang pro Pixel eines Megapixel-Bildschirms speichern, haben Sie gerade eine Million Vorgänge gespeichert. Multipliziert mit 60 Bildschirmen pro Sekunde sparen Sie noch mehr Vorgänge.)

hvd's answer liefert einige zusätzliche Beispiele.

2
TOOGAM