<=>
. Vor einiger Zeit gab es einen Beitrag von unserem eigenen Simon Brand, in dem einige Informationen zu diesem neuen Operator sowie einige konzeptionelle Informationen darüber, was er ist und tut, aufgeführt sind. Das Ziel dieses Beitrags ist es, einige konkrete Anwendungen dieses seltsamen neuen Operators und seines zugehörigen Gegenstücks, des operator==
(ja, es wurde zum Besseren geändert!) Zu untersuchen und gleichzeitig einige Richtlinien für die Verwendung im alltäglichen Code bereitzustellen.
Vergleiche
Es ist nicht ungewöhnlich, Code wie den folgenden zu sehen:
struct IntWrapper { int value; constexpr IntWrapper(int value): value{value} { } bool operator==(const IntWrapper& rhs) const { return value == rhs.value; } bool operator!=(const IntWrapper& rhs) const { return !(*this == rhs); } bool operator<(const IntWrapper& rhs) const { return value < rhs.value; } bool operator<=(const IntWrapper& rhs) const { return !(rhs < *this); } bool operator>(const IntWrapper& rhs) const { return rhs < *this; } bool operator>=(const IntWrapper& rhs) const { return !(*this < rhs); } };
Hinweis: Leser mit Adleraugen werden feststellen, dass dies sogar noch weniger ausführlich ist als im Code vor C ++ 20, da diese Funktionen eigentlich alle Freunde von Nichtmitgliedern sein sollten, dazu später mehr.
Das ist eine Menge Code, den man schreiben muss, um sicherzustellen, dass mein Typ mit etwas vom gleichen Typ vergleichbar ist. Okay, wir beschäftigen uns eine Weile damit. Dann kommt jemand, der dies schreibt:
constexpr bool is_lt(const IntWrapper& a, const IntWrapper& b) { return a < b; } int main() { static_assert(is_lt(0, 1)); }
Das erste, was Sie bemerken werden, ist, dass dieses Programm nicht kompiliert wird.
error C3615: constexpr function 'is_lt' cannot result in a constant expression
Ah! Das Problem ist, dass wir
constexpr
für unsere Vergleichsfunktion vergessen constexpr
, drat! Also fügt man allen Vergleichsoperatoren constexpr
hinzu. Ein paar Tage später fügt jemand einen is_gt
Helfer hinzu, is_gt
jedoch fest, dass alle Vergleichsoperatoren keine Ausnahmespezifikation haben, und durchläuft denselben mühsamen Prozess, noexcept
dem jeder der 5 Überladungen noexcept
hinzugefügt wird.Hier setzt der neue Raumschiff-Operator von C ++ 20 an, um uns zu helfen. Mal sehen, wie der ursprüngliche
IntWrapper
in einer C ++ 20-Welt geschrieben werden kann: #include <compare> struct IntWrapper { int value; constexpr IntWrapper(int value): value{value} { } auto operator<=>(const IntWrapper&) const = default; };
Der erste Unterschied, den Sie möglicherweise bemerken, ist die neue Aufnahme von
<compare>
. Der <compare>
-Header ist dafür verantwortlich, den Compiler mit allen Vergleichskategorietypen zu füllen, die der Raumschiffoperator benötigt, um einen für unsere Standardfunktion geeigneten Typ zurückzugeben. Im obigen Snippet wird der Rückgabetyp auto
zu std::strong_ordering
.Wir haben nicht nur 5 überflüssige Zeilen entfernt, sondern müssen auch nichts definieren, der Compiler erledigt das für uns! Unser
is_lt
bleibt unverändert und funktioniert nur, während es noch constexpr
, obwohl wir dies in unserem Standardoperator operator<=>
nicht explizit angegeben haben. Das ist gut und schön, aber einige Leute kratzen sich vielleicht am Kopf, warum is_lt
immer noch kompilieren darf, obwohl der Raumschiffoperator überhaupt nicht verwendet wird. Lassen Sie uns die Antwort auf diese Frage untersuchen.Ausdrücke umschreiben
In C ++ 20 wird dem Compiler ein neues Konzept vorgestellt, das sich auf "umgeschriebene" Ausdrücke bezieht. Der Raumschiffoperator gehört zusammen mit dem
operator==
zu den ersten beiden Kandidaten, für die umgeschriebene Ausdrücke gelten. Für ein konkreteres Beispiel für das Umschreiben von Ausdrücken wollen wir das in is_lt
bereitgestellte Beispiel is_lt
.Während der Überlastungsauflösung wählt der Compiler aus einer Reihe brauchbarer Kandidaten aus, die alle dem gesuchten Operator entsprechen. Der Kandidatensammelprozess wird für relationale und Äquivalenzoperationen geringfügig geändert, bei denen der Compiler auch spezielle umgeschriebene und synthetisierte Kandidaten sammeln muss ( [over.match.oper] /3.4 ).
Für unseren Ausdruck
a < b
der Standard an, dass wir den Typ a nach einem operator<=>
oder einem Namespace-Bereichsfunktionsoperator operator<=>
durchsuchen können, der seinen Typ akzeptiert. Der Compiler stellt also fest, dass der Typ von a IntWrapper::operator<=>
. Der Compiler darf dann diesen Operator verwenden und den Ausdruck a < b
als (a <=> b) < 0
umschreiben. Dieser umgeschriebene Ausdruck wird dann als Kandidat für eine normale Überlastungsauflösung verwendet.Möglicherweise fragen Sie sich, warum dieser umgeschriebene Ausdruck gültig und korrekt ist. Die Richtigkeit des Ausdrucks ergibt sich tatsächlich aus der Semantik, die der Raumschiffoperator bereitstellt. Das
<=>
ist ein Drei-Wege-Vergleich, der impliziert, dass Sie nicht nur ein binäres Ergebnis, sondern (in den meisten Fällen) eine Reihenfolge erhalten. Wenn Sie eine Reihenfolge haben, können Sie diese Reihenfolge in Form von relationalen Operationen ausdrücken. Ein kurzes Beispiel: Der Ausdruck 4 <=> 5 in C ++ 20 gibt Ihnen das Ergebnis std::strong_ordering::less
. Das Ergebnis std::strong_ordering::less
impliziert, dass 4
nicht nur von 5
verschieden ist, sondern strikt unter diesem Wert liegt. Dies macht die Anwendung der Operation (4 <=> 5) < 0
korrekt und genau, um unser Ergebnis zu beschreiben.Unter Verwendung der obigen Informationen kann der Compiler jeden verallgemeinerten relationalen Operator (dh
<
, >
usw.) nehmen und ihn in Bezug auf den Raumschiffoperator umschreiben. Im Standard wird der umgeschriebene Ausdruck häufig als (a <=> b) @ 0
wobei das @
eine relationale Operation darstellt.Ausdrücke synthetisieren
Die Leser haben möglicherweise die subtile Erwähnung von "synthetisierten" Ausdrücken oben bemerkt und spielen auch eine Rolle bei diesem Umschreibungsprozess des Operators. Betrachten Sie eine andere Prädikatfunktion:
constexpr bool is_gt_42(const IntWrapper& a) { return 42 < a; }
Wenn wir unsere ursprüngliche Definition für
IntWrapper
dieser Code nicht kompiliert.error C2677: binary '<': no global operator found which takes type 'const IntWrapper' (or there is no acceptable conversion)
Dies ist in Land vor C ++ 20
IntWrapper
dieses Problem zu lösen, müssen Sie IntWrapper
einige zusätzliche friend
Funktionen IntWrapper
die sich auf der linken Seite von int
. Wenn Sie versuchen, dieses Beispiel mit einem C ++ 20-Compiler und unserer C ++ 20-Definition von IntWrapper
Sie möglicherweise feststellen, dass es wieder „einfach funktioniert“ - ein weiterer Head Scratcher. Lassen Sie uns untersuchen, warum der obige Code in C ++ 20 noch kompiliert werden darf.Während der Überlastungsauflösung sammelt der Compiler auch das, was der Standard als "synthetisierte" Kandidaten bezeichnet, oder einen umgeschriebenen Ausdruck mit umgekehrter Reihenfolge der Parameter. Im obigen Beispiel versucht der Compiler, den umgeschriebenen Ausdruck
(42 <=> a) < 0
, stellt jedoch fest, dass keine Konvertierung von IntWrapper
nach int
, um die linke Seite zu erfüllen, sodass der umgeschriebene Ausdruck gelöscht wird. Der Compiler zaubert auch den "synthetisierten" Ausdruck 0 < (a <=> 42)
und stellt fest, dass über seinen Konvertierungskonstruktor eine Konvertierung von int
nach IntWrapper
, sodass dieser Kandidat verwendet wird.Das Ziel der synthetisierten Ausdrücke ist es, das Durcheinander zu vermeiden, dass das Boilerplate der
friend
Funktionen geschrieben werden muss, um Lücken zu füllen, in denen Ihr Objekt von anderen Typen konvertiert werden könnte. Synthetisierte Ausdrücke werden auf 0 @ (b <=> a)
verallgemeinert.Komplexere Typen
Der vom Compiler generierte Raumschiffoperator stoppt nicht bei einzelnen Mitgliedern von Klassen, sondern generiert einen korrekten Satz von Vergleichen für alle Unterobjekte in Ihren Typen:
struct Basics { int i; char c; float f; double d; auto operator<=>(const Basics&) const = default; }; struct Arrays { int ai[1]; char ac[2]; float af[3]; double ad[2][2]; auto operator<=>(const Arrays&) const = default; }; struct Bases : Basics, Arrays { auto operator<=>(const Bases&) const = default; }; int main() { constexpr Bases a = { { 0, 'c', 1.f, 1. }, { { 1 }, { 'a', 'b' }, { 1.f, 2.f, 3.f }, { { 1., 2. }, { 3., 4. } } } }; constexpr Bases b = { { 0, 'c', 1.f, 1. }, { { 1 }, { 'a', 'b' }, { 1.f, 2.f, 3.f }, { { 1., 2. }, { 3., 4. } } } }; static_assert(a == b); static_assert(!(a != b)); static_assert(!(a < b)); static_assert(a <= b); static_assert(!(a > b)); static_assert(a >= b); }
Der Compiler weiß, wie Mitglieder von Klassen, die Arrays sind, in ihre Listen von Unterobjekten erweitert und rekursiv verglichen werden. Wenn Sie die Körper dieser Funktionen selbst schreiben möchten, haben Sie natürlich immer noch den Vorteil, dass der Compiler Ausdrücke für Sie neu schreibt.
Sieht aus wie eine Ente, schwimmt wie eine Ente und quakt wie operator==
Einige sehr kluge Leute im Standardisierungskomitee bemerkten, dass der Raumschiffbetreiber immer einen lexikografischen Vergleich von Elementen durchführen wird, egal was passiert. Die bedingungslose Durchführung lexikografischer Vergleiche kann insbesondere mit dem Gleichheitsoperator zu ineffizientem generiertem Code führen.
Das kanonische Beispiel vergleicht zwei Zeichenfolgen. Wenn Sie die Zeichenfolge
"foobar"
und sie mit == mit der Zeichenfolge "foo"
, würde man erwarten, dass diese Operation nahezu konstant ist. Der effiziente String-Vergleichsalgorithmus lautet also:- Vergleichen Sie zuerst die Größe der beiden Zeichenfolgen, wenn sich die Größen unterscheiden, geben Sie andernfalls
false
- Gehen Sie jedes Element der beiden Zeichenfolgen gemeinsam durch und vergleichen Sie es, bis sich eines unterscheidet oder das Ende erreicht ist. Geben Sie das Ergebnis zurück.
Nach den Regeln für Raumschiffoperatoren müssen wir zuerst mit dem tiefen Vergleich jedes Elements beginnen, bis wir das andere finden. In unserem Beispiel von
"foobar"
und "foo"
nur dann false
wenn Sie 'b'
mit '\0'
.Um dem entgegenzuwirken, gab es ein Papier, P1185R2, das eine Möglichkeit für den Compiler beschreibt,
operator==
unabhängig vom Raumschiffoperator neu zu schreiben und zu generieren. Unser IntWrapper
könnte wie folgt geschrieben werden: #include <compare> struct IntWrapper { int value; constexpr IntWrapper(int value): value{value} { } auto operator<=>(const IntWrapper&) const = default; bool operator==(const IntWrapper&) const = default; };
Nur noch ein Schritt ... es gibt jedoch gute Nachrichten; Sie müssen den obigen Code nicht wirklich schreiben, da das einfache Schreiben des
auto operator<=>(const IntWrapper&) const = default
ausreicht, damit der Compiler implizit den separaten - und effizienteren - operator==
für Sie generiert!Der Compiler wendet eine leicht geänderte Regel zum Umschreiben an, die für
==
und !=
Speziell ist. Dabei werden in diesen Operatoren operator==
und nicht operator<=>
umgeschrieben. Dies bedeutet, dass !=
Auch von der Optimierung profitiert.Alter Code wird nicht brechen
An diesem Punkt könnten Sie denken, OK, wenn der Compiler dieses Operator-Umschreibungsgeschäft ausführen darf, was passiert, wenn ich versuche, den Compiler zu überlisten:
struct IntWrapper { int value; constexpr IntWrapper(int value): value{value} { } auto operator<=>(const IntWrapper&) const = default; bool operator<(const IntWrapper& rhs) const { return value < rhs.value; } }; constexpr bool is_lt(const IntWrapper& a, const IntWrapper& b) { return a < b; }
Die Antwort hier ist, dass Sie es nicht getan haben. Das Überlastungsauflösungsmodell in C ++ hat diese Arena, in der alle Kandidaten kämpfen, und in diesem speziellen Kampf haben wir 3 Kandidaten:
IntWrapper::operator<(const IntWrapper& a, const IntWrapper& b)
IntWrapper::operator<=>(const IntWrapper& a, const IntWrapper& b)
(umgeschrieben)
IntWrapper::operator<=>(const IntWrapper& b, const IntWrapper& a)
(synthetisiert)
Wenn wir die Überlastungsauflösungsregeln in C ++ 17 akzeptiert hätten, wäre das Ergebnis dieses Aufrufs nicht eindeutig gewesen, aber die C ++ 20-Überlastungsauflösungsregeln wurden geändert, damit der Compiler diese Situation auf die logischste Überladung auflösen kann.
Es gibt eine Phase der Überlastungsauflösung, in der der Compiler einen Serien-Tiebreaker ausführen muss. In C ++ 20 gibt es einen neuen Tiebreaker, der besagt, dass wir Überladungen bevorzugen müssen, die nicht neu geschrieben oder synthetisiert werden. Dies macht unseren Überlastungs-
IntWrapper::operator<
zum besten Kandidaten und löst die Mehrdeutigkeit. Dieselbe Maschinerie verhindert, dass synthetisierte Kandidaten auf reguläre umgeschriebene Ausdrücke stampfen.Gedanken schließen
Der Raumschiff-Operator ist eine willkommene Ergänzung zu C ++ und eine der Funktionen, die es Ihnen erleichtern und Ihnen helfen, weniger Code zu schreiben, und manchmal ist weniger mehr. Schnallen Sie sich also mit dem Raumschiff- Operator von C ++ 20 an!
Wir empfehlen Ihnen dringend, den Raumschiff-Operator auszuprobieren. Er ist ab sofort in Visual Studio 2019 unter
/std:c++latest
verfügbar. Hinweis: Die durch P1185R2 eingeführten Änderungen sind in Visual Studio 2019 Version 16.2 verfügbar. Bitte beachten Sie, dass der Raumschiffoperator Teil von C ++ 20 ist und bis zu dem Zeitpunkt, an dem C ++ 20 abgeschlossen ist, einigen Änderungen unterworfen ist.Wie immer freuen wir uns über Ihr Feedback. Sie können Kommentare per E-Mail an visualcpp@microsoft.com , über Twitter @visualc oder Facebook an Microsoft Visual Cpp senden . Folgen Sie mir auch auf Twitter @starfreakclone .
Wenn Sie in VS 2019 auf andere Probleme mit MSVC stoßen, teilen Sie uns dies bitte über die Option Problem melden mit, entweder vom Installationsprogramm oder von der Visual Studio-IDE selbst. Für Vorschläge oder Fehlerberichte lassen Sie es uns über DevComm wissen .