. Vor nicht allzu langer Zeit veröffentlichte Simon Brand einen Beitrag , der detaill...">

Der neue Raumschiffoperator in C ++ 20

C ++ 20 fügt einen neuen Operator namens "Raumschiff" hinzu: <=> . Vor nicht allzu langer Zeit veröffentlichte Simon Brand einen Beitrag , der detaillierte konzeptionelle Informationen darüber enthielt, was dieser Operator ist und für welche Zwecke er verwendet wird. Die Hauptaufgabe dieses Beitrags besteht darin, die spezifischen Anwendungen des „seltsamen“ neuen Operators und seines analogen operator== und einige Empfehlungen für seine Verwendung in der täglichen Codierung zu formulieren.


Vergleich


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: Aufmerksame Leser werden feststellen, dass dies sogar noch weniger ausführlich ist, als es im Code vor C ++ 20 sein sollte. Dazu später mehr.

Sie müssen viel Standardcode schreiben, um sicherzustellen, dass unser Typ mit etwas vom gleichen Typ vergleichbar ist. Ok, wir werden es für eine Weile herausfinden. Dann kommt jemand, der so 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 das Programm nicht kompiliert wird.

error C3615: constexpr function 'is_lt' cannot result in a constant expression

Das Problem ist, dass constexpr in der Vergleichsfunktion vergessen wurde. Dann fügen constexpr allen Vergleichsoperatoren constexpr hinzu. Einige Tage später wird jemand den is_gt hinzufügen. is_gt jedoch, dass nicht alle Vergleichsoperatoren eine Ausnahmespezifikation haben und Sie denselben mühsamen Prozess noexcept , bei dem jeder der 5 Überladungen noexcept hinzugefügt wird.

Hier hilft uns der neue C ++ 20-Raumschiffbetreiber. Mal sehen, wie Sie den ursprünglichen IntWrapper in der C ++ 20-Welt schreiben können:

 #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 Arten von Vergleichskategorien zu füllen, die für den Raumschiffoperator erforderlich sind, damit er einen Typ zurückgibt, der für unsere Standardfunktion geeignet ist. Im obigen Snippet std::strong_ordering der Rückgabetyp von auto std::strong_ordering .

Wir haben nicht nur 5 zusätzliche Zeilen gelöscht, sondern müssen auch nicht einmal etwas bestimmen, der Compiler wird dies für uns tun. is_lt bleibt unverändert und funktioniert nur, während is_lt bleibt, obwohl wir dies in unserem Standardoperator operator<=> nicht explizit angegeben haben. Das ist gut, aber einige Leute is_lt vielleicht, warum is_lt kompiliert werden darf, auch wenn der Raumschiffoperator überhaupt nicht verwendet wird. Lassen Sie uns die Antwort auf diese Frage finden.

Ausdrücke umschreiben


In C ++ 20 wird der Compiler in ein neues Konzept eingeführt, das sich auf „umgeschriebene“ Ausdrücke bezieht. Der Raumschiffoperator ist zusammen mit operator== einer der ersten beiden Kandidaten, die neu geschrieben werden können. Ein genaueres Beispiel für das Umschreiben von Ausdrücken finden Sie in dem Beispiel in is_lt .

Während der Behebung der Überlastung wählt der Compiler aus einer Reihe der am besten geeigneten Kandidaten aus, von denen jeder dem von uns benötigten Operator entspricht. Der Auswahlprozess ändert sich für Vergleichs- und Äquivalenzoperationen geringfügig, wenn der Compiler auch spezielle transkribierte und synthetisierte Kandidaten sammeln muss ( [over.match.oper] /3.4 ).

Für unseren Ausdruck a < b Standard an, dass wir nach Typ a für operator<=> oder operator<=> suchen können, die diesen Typ akzeptieren. Dies macht der Compiler und stellt fest, dass der Typ a tatsächlich 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.

Sie können fragen, warum dieser umgeschriebene Ausdruck korrekt ist. Die Richtigkeit des Ausdrucks ergibt sich tatsächlich aus der Semantik, die der Raumschiffoperator bereitstellt. <=> ist ein Drei-Wege-Vergleich, der impliziert, dass Sie nicht nur ein binäres Ergebnis erhalten, sondern auch eine Reihenfolge (in den meisten Fällen). Wenn Sie eine Bestellung haben, können Sie diese Bestellung in Form von Vergleichsvorgängen ausdrücken. Ein kurzes Beispiel: Ausdruck 4 <=> 5 in C ++ 20 gibt das Ergebnis std::strong_ordering::less . Das Ergebnis von std::strong_ordering::less impliziert, dass 4 nicht nur von 5 verschieden ist, sondern auch strikt unter diesem Wert liegt, wodurch die Anwendung der Operation (4 <=> 5) < 0 korrekt und genau ist, um unser Ergebnis zu beschreiben.

Unter Verwendung der obigen Informationen kann der Compiler jeden verallgemeinerten Vergleichsoperator (d. H. < , > Usw.) nehmen und ihn in Bezug auf den Raumschiffoperator umschreiben. Im Standard wird ein umgeschriebener Ausdruck häufig als (a <=> b) @ 0 wobei @ eine beliebige Vergleichsoperation darstellt.

Ausdrücke synthetisieren


Die Leser haben möglicherweise einen subtilen Verweis auf die oben genannten „synthetisierten“ Ausdrücke bemerkt und spielen auch eine Rolle bei diesem Prozess des Umschreibens von Aussagen. Betrachten Sie die folgende Funktion:

 constexpr bool is_gt_42(const IntWrapper& a) {  return 42 < a; } 

Wenn wir unsere ursprüngliche Definition für IntWrapper , wird dieser Code nicht kompiliert.

error C2677: binary '<': no global operator found which takes type 'const IntWrapper' (or there is no acceptable conversion)

Dies ist 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 des int . Wenn Sie versuchen, dieses Beispiel mit dem Compiler und der IntWrapper C ++ 20-Definition zu IntWrapper , stellen Sie möglicherweise fest, dass es wieder funktioniert. Schauen wir uns an, warum der obige Code in C ++ 20 noch kompiliert wird.

Beim Auflösen von Überladungen sammelt der Compiler auch das, was der Standard als "synthetisierte" Kandidaten bezeichnet, oder einen neu geschriebenen Ausdruck mit der umgekehrten 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 verworfen wird. Der Compiler ruft auch den "synthetisierten" Ausdruck 0 < (a <=> 42) und erkennt, dass eine Konvertierung von int nach IntWrapper über seinen Konvertierungskonstruktor erfolgt, sodass dieser Kandidat verwendet wird.

Der Zweck synthetisierter Ausdrücke besteht darin, die Verwirrung beim Schreiben von Vorlagen für Freundfunktionen zu vermeiden, um die Lücken zu füllen, in die Ihr Objekt von anderen Typen konvertiert werden kann. 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 die richtigen Vergleiche 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 Klassenmitglieder, die Arrays sind, in ihre Liste von Unterobjekten erweitert und rekursiv verglichen werden. Wenn Sie die Hauptteile dieser Funktionen selbst schreiben möchten, profitieren Sie natürlich weiterhin vom Umschreiben von Ausdrücken durch den Compiler.

Sieht aus wie eine Ente, schwimmt wie eine Ente und quakt wie operator==


Einige sehr kluge Leute im Standardisierungskomitee haben bemerkt, dass der Raumschiffbetreiber immer einen lexikografischen Vergleich von Elementen durchführen wird, egal was passiert. Die bedingungslose Ausführung von lexikografischen Vergleichen kann insbesondere mit dem Gleichheitsoperator zu ineffizientem Code führen.

Ein kanonisches Beispiel für den Vergleich zweier Zeilen. Wenn Sie die Zeichenfolge "foobar" und sie mit == mit der Zeichenfolge "foo" vergleichen, können Sie davon ausgehen, dass diese Operation nahezu konstant ist. Ein effektiver String-Vergleichsalgorithmus lautet wie folgt:

  • Vergleichen Sie zunächst die Größe der beiden Zeilen. Wenn die Größen unterschiedlich sind, geben Sie false
  • Andernfalls gehen Sie Schritt für Schritt durch jedes Element aus zwei Zeilen und vergleichen Sie sie, bis ein Unterschied vorliegt oder alle Elemente enden. Geben Sie das Ergebnis zurück.

In Übereinstimmung mit den Regeln des Raumschiffoperators müssen wir zunächst jedes Element vergleichen, bis wir eines finden, das anders ist. In unserem Beispiel "foobar" und "foo" nur beim Vergleich von 'b' und '\0' schließlich false .

Um dem entgegenzuwirken, gab es Artikel P1185R2 , in dem detailliert beschrieben wird, wie der Compiler den operator== unabhängig vom Raumschiffoperator neu schreibt und generiert. Unser IntWrapper kann 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; }; 

Noch ein Schritt ... es gibt jedoch gute Nachrichten; Sie müssen den obigen Code nicht wirklich schreiben, da das Schreiben des auto operator<=>(const IntWrapper&) const = default ausreicht, damit der Compiler implizit einen separaten und effizienteren operator== für Sie generiert!

Der Compiler wendet eine leicht modifizierte "Umschreib" -Regel an, die spezifisch für == und != Ist. In diesen Operatoren werden sie als operator<=> operator== und nicht als operator<=> umgeschrieben. Dies bedeutet, dass != Auch von der Optimierung profitiert.

Alter Code wird nicht kaputt gehen


An dieser Stelle könnte man denken: Nun, wenn der Compiler diese Operation zum Umschreiben des Operators 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 ist keine große Sache. Das Überlastungsauflösungsmodell in C ++ ist die Arena, in der alle Kandidaten aufeinander treffen. In dieser besonderen Schlacht haben wir drei davon:

  • 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 in C ++ 17 Überlastungsauflösungsregeln übernehmen würden, wäre das Ergebnis dieses Aufrufs gemischt, aber die C ++ 20-Überlastungsauflösungsregeln wurden geändert, damit der Compiler diese Situation auf die logischste Überladung auflösen kann.

Es gibt eine Überlastungsauflösungsphase, in der der Compiler eine Reihe zusätzlicher Durchgänge ausführen muss. In C ++ 20 ist ein neuer Mechanismus erschienen, bei dem Überladungen bevorzugt werden, die nicht überschrieben oder synthetisiert werden. IntWrapper::operator< wird unser IntWrapper::operator< Überladen zum besten Kandidaten und löst Mehrdeutigkeiten auf. Der gleiche Mechanismus verhindert die Verwendung synthetisierter Kandidaten anstelle der üblichen umgeschriebenen Ausdrücke.

Letzte Gedanken


Der Raumschiffoperator ist eine willkommene Ergänzung zu C ++, da er dazu beitragen kann, Ihren Code zu vereinfachen und weniger zu schreiben, und manchmal ist weniger besser. Also schnall dich an und kontrolliere dein C ++ 20 Raumschiff !

Wir empfehlen Ihnen dringend, den Raumschiff-Operator auszuprobieren. Er ist ab sofort in Visual Studio 2019 unter /std:c++latest verfügbar. Hinweis: Änderungen am P1185R2 sind in Visual Studio 2019 Version 16.2 verfügbar. Bitte beachten Sie, dass der Raumschiffoperator Teil von C ++ 20 ist und bis zum Abschluss von C ++ 20 einige Änderungen unterliegen kann.

Wie immer erwarten wir Ihr Feedback. Sie können Kommentare per E-Mail an visualcpp@microsoft.com , über Twitter @visualc oder Facebook Microsoft Visual Cpp senden .

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 schreiben Sie uns über DevComm.

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


All Articles