. Vor einiger Zeit gab es einen Beitrag von uns...">

Vereinfachen Sie Ihren Code mit Rocket Science: C ++ 20s Raumschiff-Operator

C ++ 20 fügt einen neuen Operator hinzu, der liebevoll als "Raumschiff" -Operator bezeichnet wird: <=> . 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 .

Source: https://habr.com/ru/post/de458248/


All Articles