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.

In Pocket speichern vorlesen Druckansicht 37 Kommentare lesen

(Bild: Sinart Creative/Shutterstock.com)

Lesezeit: 3 Min.
Von
  • Rainer Grimm
Inhaltsverzeichnis

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.

Modernes C++ – Rainer Grimm

Rainer Grimm ist seit vielen Jahren als Softwarearchitekt, Team- und Schulungsleiter tätig. Er schreibt gerne Artikel zu den Programmiersprachen C++, Python und Haskell, spricht aber auch gerne und häufig auf Fachkonferenzen. Auf seinem Blog Modernes C++ beschäftigt er sich intensiv mit seiner Leidenschaft C++.

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.

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.

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)