Programmiersprache C++: Der automatisch generierte Gleichheitsoperator
In C++20 lässt sich neben dem Drei-Wege-Vergleichsoperator auch der Gleichheitsoperator vom Compiler anfordern oder definieren.
- Rainer Grimm
Die meisten C++-Entwicklerinnen und -Entwickler dĂĽrften damit vertraut sein, dass sich der Drei-Wege-Vergleichsoperator definieren oder mit =default
vom Compiler anfordern lässt. Weniger bekannt ist vermutlich, dass sich in C++20 auch der Gleichheitsoperator definieren oder anfordern lässt?
Bevor ich auf den automatisch generierten Gleichheitsoperator eingehe, möchte ich die wichtigsten Fakten zum Drei-Wege-Vergleichsoperator auffrischen.
Der Drei-Wege-Vergleichsoperator
Der Drei-Wege-Vergleichsoperator lässt sich definieren oder mit =default
vom Compiler anfordern. In beiden Fällen erhält man alle sechs Vergleichsoperatoren: ==, !=, <, <=, >,
und >=
.
// threeWayComparison.cpp
#include <compare>
#include <iostream>
struct MyInt {
int value;
explicit MyInt(int val): value{val} { }
auto operator<=>(const MyInt& rhs) const { // (1)
return value <=> rhs.value;
}
};
struct MyDouble {
double value;
explicit constexpr MyDouble(double val): value{val} { }
auto operator<=>(const MyDouble&) const = default; // (2)
};
template <typename T>
constexpr bool isLessThan(const T& lhs, const T& rhs) {
return lhs < rhs;
}
int main() {
std::cout << std::boolalpha << std::endl;
MyInt myInt1(2011);
MyInt myInt2(2014);
std::cout << "isLessThan(myInt1, myInt2): "
<< isLessThan(myInt1, myInt2) << std::endl;
MyDouble myDouble1(2011);
MyDouble myDouble2(2014);
std::cout << "isLessThan(myDouble1, myDouble2): "
<< isLessThan(myDouble1, myDouble2) << std::endl;
std::cout << std::endl;
}
Der benutzerdefinierte (1) und der vom Compiler erzeugte (2) Drei-Wege-Vergleichsoperator funktionieren, wie erwartet.
Es gibt aber ein paar beachtenswerte Unterschiede zwischen den beiden Drei-Wege-Vergleichsoperatoren. Der vom Compiler deduzierte RĂĽckgabetyp fĂĽr MyInt
(1) unterstĂĽtzt die strenge Ordnung, der vom Compiler deduzierte RĂĽckgabetyp fĂĽr MyDouble
(2) unterstützt hingegen nur die partielle Ordnung. Gleitkommazahlen können nur die partielle Ordnung unterstützen, da sich Werte wie NaN (Not a Number) nicht ordnen lassen. Zum Beispiel gilt, dass NaN == NaN
zu false
evaluiert.
Der vom Compiler erzeugte Drei-Wege-Vergleichsoperator, der implizit constexpr
und noexcept
ist, benötigt den Header <compare>.
AuĂźerdem fĂĽhrt er einen lexikografischen Vergleich durch. Lexikografischer Vergleich bedeutet in diesem Zusammenhang, dass alle Basisklassen von links nach rechts und alle nicht-statischen Member in der Reihenfolge ihrer Deklaration verglichen werden.
Nehmen wir an, ich fĂĽge den beiden Klassen MyInt
und MyDouble
ein std::unordered_set
hinzu.
struct MyInt {
int value;
std::unordered_set<int> mySet;
explicit MyInt(int val): value{val}, mySet{val} { }
bool operator<=>(const MyInt& rhs) const {
if (auto first = value <=> rhs.value; first != 0) return first;
else return mySet <=> rhs.mySet;
}
};
struct MyDouble {
double value;
std::unordered_set<double> mySet;
explicit MyDouble(double val): value{val}, mySet{val} { }
bool operator<=>(const MyDouble&) const = default;
};
Anfordern oder Definieren des Drei-Wege-Vergleichs schlägt fehl, weil std::unordered_set
keine Ordnung unterstĂĽtzt. std::unordered_set
unterstĂĽtzt nur Gleichheitsvergleiche, und das gilt somit auch fĂĽr MyInt
und MyDouble
.
Gleichheitsoperator
Wird der Gleichheitsoperator definiert oder vom Compiler mit =default
angefordert, erhält man automatisch die Gleichheits- und Ungleichheitsoperatoren: ==
, und !=
.
// equalityOperator.cpp
#include <iostream>
#include <tuple>
#include <unordered_set>
struct MyInt {
int value;
std::unordered_set<int> mySet;
explicit MyInt(int val): value{val}, mySet{val} { }
bool operator==(const MyInt& rhs) const {
return std::tie(value, mySet) == std::tie(rhs.value, rhs.mySet);
}
};
struct MyDouble {
double value;
std::unordered_set<double> mySet;
explicit MyDouble(double val): value{val}, mySet{val} { }
bool operator==(const MyDouble&) const = default;
};
template <typename T>
constexpr bool areEqual(const T& lhs, const T& rhs) {
return lhs == rhs;
}
template <typename T>
constexpr bool areNotEqual(const T& lhs, const T& rhs) {
return lhs != rhs;
}
int main() {
std::cout << std::boolalpha << '\n';
MyInt myInt1(2011);
MyInt myInt2(2014);
std::cout << "areEqual(myInt1, myInt2): "
<< areEqual(myInt1, myInt2) << '\n';
std::cout << "areNotEqual(myInt1, myInt2): "
<< areNotEqual(myInt1, myInt2) << '\n';
std::cout << '\n';
MyDouble myDouble1(2011.0);
MyDouble myDouble2(2014.0);
std::cout << "areEqual(myDouble1, myDouble2): "
<< areEqual(myDouble1, myDouble2) << '\n';
std::cout << "areNotEqual(myDouble1, myDouble2): "
<< areNotEqual(myDouble1, myDouble2) << '\n';
std::cout << '\n';
}
Jetzt kann ich MyInt
und MyDouble
auf Gleichheit und Ungleichheit vergleichen.
Im Programm equalityOperator.cpp
habe ich einen Trick angewendet – Wer erkennt ihn?
In dem folgenden Beispiel habe ich den Gleichheitsoperator von MyInt
implementiert, indem ich die Gleichheitsoperatoren von value
und mySet
verkettet habe.
struct MyInt {
int value;
std::unordered_set<int> mySet;
explicit MyInt(int val): value{val}, mySet{val} { }
bool operator==(const MyInt& rhs) const {
if (auto first = value == rhs.value; first != 0) return first;
else return mySet == rhs.mySet;
}
};
Das ist ziemlich fehleranfällig und sieht unschön aus, wenn man eine Klasse mit mehreren Membern hat.
Im Gegensatz dazu habe ich std::tie
verwendet, um den Gleichheitsoperator im Programm equalityOperator.cpp
zu implementieren.
struct MyInt {
int value;
std::unordered_set<int> mySet;
explicit MyInt(int val): value{val}, mySet{val} { }
bool operator==(const MyInt& rhs) const {
return std::tie(value, mySet) == std::tie(rhs.value, rhs.mySet);
}
};
std::tie
erstellt ein Tupel von lvalue
-Referenzen zu seinen Argumenten. Zum Schluss werden die erzeugten Tupel lexikografisch verglichen.
Wie geht's weiter?
In meinem nächsten Artikel werde ich meine Reise durch C++20 fortsetzen und über std::span
schreiben. std::span
stellt ein Objekt dar, das sich auf eine zusammenhängende Folge von Objekten bezieht.
(map)