C++20: Mehr Details zum Spaceship Operator
Der Compiler führt beim Erzeugen der sechs Vergleichsoperatoren einen sehr smarten Job aus. Letztlich erzeugt er gratis intuitive und effiziente Vergleichsoperatoren. Eine genauere Betrachtung des Spaceship Operator.
Der Compiler führt beim Erzeugen der sechs Vergleichsoperatoren einen sehr smarten Job aus. Letztlich erzeugt er gratis intuitive und effiziente Vergleichsoperatoren. Eine genauere Betrachtung des Spaceship Operator.
Zuerst möchte ich in dem Artikel auf einen Punkt eingehen, den ich wohl schon in meinem letzten Artikel "C++20: Der Drei-Weg-Vergleichsoperator <=> [1]" hätte vorstellen sollen.
![C++20: Mehr Details zum Spaceship Operator](https://heise.cloudimg.io/width/696/q85.png-lossy-85.webp-lossy-85.foil1/_www-heise-de_/imgs/18/2/9/1/4/8/9/3/TimelineCpp20-468453a683e4be9e.png)
Direkte Anwendung des Drei-Weg-Vergleichsoperators
Der Drei-Weg-Vergleichsoperator lässt sich auch direkt anwenden:
// spaceship.cpp
#include <compare>
#include <iostream>
#include <string>
#include <vector>
int main() {
std::cout << std::endl;
int a(2011);
int b(2014);
auto res = a <=> b; // (1)
if (res < 0) std::cout << "a < b" << std::endl;
else if (res == 0) std::cout << "a == b" << std::endl;
else if (res > 0) std::cout << "a > b" << std::endl;
std::string str1("2014");
std::string str2("2011");
auto res2 = str1 <=> str2; // (2)
if (res2 < 0) std::cout << "str1 < str2" << std::endl;
else if (res2 == 0) std::cout << "str1 == str2" << std::endl;
else if (res2 > 0) std::cout << "str1 > str2" << std::endl;
std::vector<int> vec1{1, 2, 3};
std::vector<int> vec2{1, 2, 3};
auto res3 = vec1 <=> vec2; // (3)
if (res3 < 0) std::cout << "vec1 < vec2" << std::endl;
else if (res3 == 0) std::cout << "vec1 == vec2" << std::endl;
else if (res3 > 0) std::cout << "vec1 > vec2" << std::endl;
std::cout << std::endl;
}
Der Spaceship Operator lässt sich direkt für ints (1), Strings (2) und Vektoren (3) anwenden. Dank des Wandbox-Online-Compilers und des neuesten GCC folgt hier auch schon die Ausgabe des Programms:
![C++20: Mehr Details zum Spaceship Operator](https://heise.cloudimg.io/width/298/q85.png-lossy-85.webp-lossy-85.foil1/_www-heise-de_/imgs/18/2/9/1/4/8/9/3/spaceship-619395b7d032e735.png)
Jetzt stelle ich aber etwas Neues vor. C++20 führt das Konzept von "Rewritten Expressions" ein.
Ausdrücke umschreiben
Wenn der Compiler einen Ausdruck wie a <
b
parst, schreibt er ihn in einen Ausdruck (a <=> b) < 0
um und wendet dabei den Spacehip Operator an. Diese Regel gilt für alle sechs Vergleichsoperatoren: a OP b
wird zu (a <=> b) OP 0
. Das ist noch nicht die ganze Magie. Falls es keine Konvertierung von Datentyp (a) zu Datentyp (b) gibt, erzeugt der Compiler den Ausdruck 0 OP (b <=> a)
.
Das heißt zum Beispiel für den Kleiner-als-Operator, dass der Compiler 0 < (b <=> a)
erzeugt, falls (a <=> b) < 0
nicht möglich ist. Damit kümmert sich der Compiler automatisch um die Symmetrie der Vergleichsoperatoren. Das folgende Beispiel zeigt dieses Umschreiben der Ausdrücke in Aktion:
// rewrittenExpressions.cpp
#include <compare>
#include <iostream>
class MyInt {
public:
constexpr MyInt(int val): value{val} { }
auto operator<=>(const MyInt& rhs) const = default;
private:
int value;
};
int main() {
std::cout << std::endl;
constexpr MyInt myInt2011(2011);
constexpr MyInt myInt2014(2014);
constexpr int int2011(2011);
constexpr int int2014(2014);
if (myInt2011 < myInt2014) std::cout << "myInt2011 < myInt2014" << std::endl; // (1)
if ((myInt2011 <=> myInt2014) < 0) std::cout << "myInt2011 < myInt2014" << std::endl;
std::cout << std::endl;
if (myInt2011 < int2014) std:: cout << "myInt2011 < int2014" << std::endl; // (2)
if ((myInt2011 <=> int2014) < 0) std:: cout << "myInt2011 < int2014" << std::endl;
std::cout << std::endl;
if (int2011 < myInt2014) std::cout << "int2011 < myInt2014" << std::endl; // (3)
if (0 < (myInt2014 <=> int2011)) std:: cout << "int2011 < myInt2014" << std::endl; // (4)
std::cout << std::endl;
}
An den Stellen (1), (2) und (3) habe ich den Kleiner-als-Operator und den entsprechenden Ausdruck für den Spaceship Operator verwendet. (4) ist besonders interessant. Das Beispiel veranschaulicht, wie der
Vergleich (int2011 < myInt2014
) das Erzeugen des Spaceship-Ausdrucks (0 < (myInt2014 <=> int2011
) anstößt.
Um ehrlich zu sein, besitzt MyInt
ein Designproblem. Konstruktoren, die ein Argument annehmen, sollten explizit deklariert werden.
Explizite Konstruktoren
Konstruktoren, die wie MyInt(int val)
nur ein Argument annehmen, sind Konvertierungskonstruktoren. Das heißt in dem konkreten Fall, dass sich MyInt
mit jeder Ganz- oder Gleitkommazahl erzeugen lässt. Der Grund ist, dass jede Ganzzahl oder Gleitkommazahl sich in ein int
konvertieren lässt. Diese implizite Konvertierung nach int
ist in der Regel nicht im Sinne des Klassendesigners.
Erster Versuch
Um die implizite Konvertierung zu unterbinden, annotiere ich den Konstruktor mit dem explicit
-Schlüsselwort und folge damit Metaregel von Python: "explicit is better than implicit". Im folgenden Beispiel kommt der explizite Konstruktor zum Einsatz:
// threeWayComparisonWithInt1.cpp
#include <compare>
#include <iostream>
class MyInt {
public:
constexpr explicit MyInt(int val): value{val} { }
auto operator<=>(const MyInt& rhs) const = default;
private:
int value;
};
template <typename T, typename T2>
constexpr bool isLessThan(const T& lhs, const T2& rhs) {
return lhs < rhs; // (1)
}
int main() {
std::cout << std::boolalpha << std::endl;
constexpr MyInt myInt2011(2011);
constexpr MyInt myInt2014(2014);
constexpr int int2011(2011);
constexpr int int2014(2014);
std::cout << "isLessThan(myInt2011, myInt2014): "
<< isLessThan(myInt2011, myInt2014) << std::endl;
std::cout << "isLessThan(int2011, myInt2014): "
<< isLessThan(int2011, myInt2014) << std::endl; // (3)
std::cout << "isLessThan(myInt2011, int2014): "
<< isLessThan(myInt2011, int2014) << std::endl; // (2)
constexpr auto res = isLessThan(myInt2011, int2014);
std::cout << std::endl;
}
Das war einfach. Dank des expliziten Konstruktors ist die implizite Konvertierung von int
nach MyInt
in (1) nicht mehr zulässig. Der Compiler gibt mir das deutlich zu verstehen:
![C++20: Mehr Details zum Spaceship Operator](https://heise.cloudimg.io/width/696/q85.png-lossy-85.webp-lossy-85.foil1/_www-heise-de_/imgs/18/2/9/1/4/8/9/3/threeWayComparisonWithInt1-060ef81069db35d2.png)
Wenn du sorgfältig die Fehlermeldung liest, fällt auf, dass es keinen Operator <
für einen rechten int
-Operanden gibt. Darüber hinaus kann der Compiler keine Konvertierung von int
nach MyInt
anwenden. Interessanterweise beschwert er sich zuerst über den Ausdruck (2) und nicht den Ausdruck (3). Beide sind aber nicht zulässig.
Zweiter Versuch
Um Vergleiche von MyInt
und int
s zu ermöglichen, benötigt MyInt
einen zusätzlichen Drei-Weg-Vergleichsoperator:
#include <compare>
#include <iostream>
class MyInt {
public:
constexpr explicit MyInt(int val): value{val} { }
auto operator<=>(const MyInt& rhs) const = default; // (4)
constexpr auto operator<=>(const int& rhs) const { // (1)
return value <=> rhs;
}
private:
int value;
};
template <typename T, typename T2>
constexpr bool isLessThan(const T& lhs, const T2& rhs) {
return lhs < rhs;
}
int main() {
std::cout << std::boolalpha << std::endl;
constexpr MyInt myInt2011(2011);
constexpr MyInt myInt2014(2014);
constexpr int int2011(2011);
constexpr int int2014(2014);
std::cout << "isLessThan(myInt2011, myInt2014): "
<< isLessThan(myInt2011, myInt2014) << std::endl; // (3)
std::cout << "isLessThan(int2011, myInt2014): "
<< isLessThan(int2011, myInt2014) << std::endl; // (3)
std::cout << "isLessThan(myInt2011, int2014): "
<< isLessThan(myInt2011, int2014) << std::endl; // (3)
constexpr auto res = isLessThan(myInt2011, int2014); // (2)
std::cout << std::endl;
}
In (1) habe ich den Drei-Weg-Vergleichsoperator definiert und ihn als constexpr
deklariert. Der benutzerdefinierte Drei-Weg-Vergleichsoperator ist im Gegensatz zum Compiler-erzeugten nicht constexpr
. Konsequenterweise kann ich die Funktion isLessThan
(4) zur Compile-Zeit ausführen. Die Vergleiche von MyInt
s und int
s sind nun in allen Kombinationen möglich:
![C++20: Mehr Details zum Spaceship Operator](https://heise.cloudimg.io/width/696/q85.png-lossy-85.webp-lossy-85.foil1/_www-heise-de_/imgs/18/2/9/1/4/8/9/3/threeWayComparisonWithInt2-ee196e5ac8ff3208.png)
Ich finde die Implementierung der verschiedenen Drei-Weg-Vergleichsoperatoren sehr elegant. Der Compiler erzeugt den Vergleich mit MyI
nt
s, der Benutzer definiert den Vergleich mit int
s. In diesem Fall ist es ausreichend, zwei Operatoren zu definieren und damit 18 = 3 * 6 Kombinationen von Vergleichsoperatoren zu erhalten. 3 steht für die Kombination für int
s und MyInt
s, in der zumindest ein MyInt
beteiligt ist, und 6 für die Vergleichsoperatoren. In meinem letzten Artikel "C++20: Der Drei-Weg-Vergleichsoperator <=> [2]" ging ich auf die 18 Vergleichsoperatoren ein, die es vor C++20 in diesem Fall zu implementieren galt.
Ein Punkt muss ich noch hervorheben: Mit der jetzigen Implementierung lässt sich immer MyInt
mit jedem Datentyp vergleichen, der sich zu int
konvertieren lässt.
Stopp! Du magst dich jetzt fragen, warum die aktuelle Implementierung, die auf einem expliziten Konstruktor basiert,
class MyInt {
public:
constexpr explicit MyInt(int val): value{val} { }
auto operator<=>(const MyInt& rhs) const = default;
constexpr auto operator<=>(const int& rhs) const {
return value <=> rhs;
}
private:
int value;
};
besser ist als die ursprüngliche Implementierung. Diese verwendet einen Konstruktor, der implizite Konvertierungen erlaubt? Beide Implementierung erlauben den Vergleich mit Ganz- und Gleitkommazahlen:
class MyInt {
public:
constexpr MyInt(int val): value{val} { }
auto operator<=>(const MyInt& rhs) const = default;
private:
int value;
};
Wie geht's weiter?
Es gibt einen feinen Unterschied zwischen dem expliziten und dem nicht expliziten Konstruktor im Fall von MyInt
. Dieser Unterschied wird deutlich, wenn ich in meinem nächsten Artikel MyInt int
ähnlich entwerfe. Dies sind aber nicht alle Punkte: Der Compiler erzeugt aus Performanzgründen spezielle ==
- und !=
-Operatoren. Darüber hinaus verdient das Zusammenspiel der klassischen und der Drei-Weg-Vergleichsoperatoren einen eigenen Artikel.
( [3])
URL dieses Artikels:
https://www.heise.de/-4790117
Links in diesem Artikel:
[1] https://heise.de/-4782690
[2] https://heise.de/-4782690
[3] mailto:rainer@grimm-jaud.de
Copyright © 2020 Heise Medien