C++20: Die Concepts Equal und Ordering definieren

Nach der Definition des Concepts Equal geht unser Blogger Rainer Grimm einen Schritt weiter und verwendet das Concept Equal, um das Concept Ordering zu definieren.

In Pocket speichern vorlesen Druckansicht 12 Kommentare lesen
Lesezeit: 6 Min.
Von
  • Rainer Grimm
Inhaltsverzeichnis

In meinem letzten Artikel habe ich das Concept Equal definiert. Heute gehe ich einen Schritt weiter und verwende das Concept Equal, um das Concept Ordering zu definieren.

Zur Erinnerung: Das folgende Beispiel zeigt das Concept Equal und die Funktion areEqual aus dem letzten Artikel:

template<typename T>
concept Equal =
requires(T a, T b) {
{ a == b } -> bool;
{ a != b } -> bool;
};


bool areEqual(Equal auto fir, Equal auto sec) {
return fir == sec;
}

Ich wendete das Concept Equal in meinem letzten Artikel auf falsche Art an. Es fordert von a und b, dass beide denselben Datentyp besitzen müssen. Die Funktion areEqual erlaubt es aber, dass fir und sec verschiedene Datentypen sein können. Beide müssen natürlich das Concept Equal unterstützen. Die Verwendung eines Constrained Template anstelle der Placeholder-Syntax löst das Problem:

template <Equal T>
bool areEqual(T fir, T sec) {
fir == sec;
}

fir und sec müssen jetzt denselben Datentyp besitzen.

(Vielen Dank an Corentin Jabot, der mich auf diese Asymmetrie aufmerksam gemacht hat.)

Zusätzlich sollte das Concept Equal nicht prüfen, ob der Gleichheits- und Ungleichheitsoperator bool zurückgibt. Das Concept sollte prüfen, ob sich der Rückgabewert zu bool implizit oder explizit konvertieren lässt. Hier ist die verbesserte Definition des Concepts:

template<typename T>
concept Equal =
requires(T a, T b) {
{ a == b } -> std::convertible_to<bool>;
{ a != b } -> std::convertible_to<bool>;
};

std::convertible_to ist selbst ein Concept des C++20-Standards und benötigt daher die Headerdatei <concepts>:

template <class From, class To>
concept convertible_to =
std::is_convertible_v<From, To> &&
requires(From (&f)()) {
static_cast<To>(f());
};

Der C++20-Standard definiert bereits zwei Concepts für Gleichheit:

  • std::equality_comparable: entspricht meinem Concept Equal.
  • std::equality_comparable_with: erlaubt den Vergleich von Werten mit verschiedenen Datentypen; zum Beispiel: 1.0 == 1.0f.

Ich habe meinen letzten Artikel damit beendet, dass ich einen Teil von Haskells Typklassen-Hierarchie vorgestellt habe:

Die Klassenhierarchie zeigt, dass die Typklasse Ord eine Verfeinerung der Typklasse Eq ist. Dies lässt sich elegant in Haskell ausdrücken:

class Eq a where
(==) :: a -> a -> Bool
(/=) :: a -> a -> Bool

class Eq a => Ord a where
compare :: a -> a -> Ordering
(<) :: a -> a -> Bool
(<=) :: a -> a -> Bool
(>) :: a -> a -> Bool
(>=) :: a -> a -> Bool
max :: a -> a -> a

Hier ist meine Herausforderung: Lässt sich diese Beziehung ähnlich elegant in C++ mit Concepts ausdrücken? Der Einfachheit halber ignoriere ich die Funktionen compare und max aus Haskells Typklasse Ord.

Dank der requires-expressions besitzt die Definition des Concepts Ordering eine große Ähnlichkeit mit der Definition der Typklasse Ord.

template <typename T>
concept Ordering =
Equal<T> &&
requires(T a, T b) {
{ a <= b } -> bool;
{ a < b } -> bool;
{ a > b } -> bool;
{ a >= b } -> bool;
};

Das folgende Beispiel zeigt das Concept Ordering in Aktion:

// conceptsDefinitionOrdering.cpp

#include <concepts>
#include <iostream>
#include <unordered_set>

template<typename T>
concept Equal =
requires(T a, T b) {
{ a == b } -> std::convertible_to<bool>;
{ a != b } -> std::convertible_to<bool>;
};


template <typename T>
concept Ordering =
Equal<T> &&
requires(T a, T b) {
{ a <= b } -> std::convertible_to<bool>;
{ a < b } -> std::convertible_to<bool>;
{ a > b } -> std::convertible_to<bool>;
{ a >= b } -> std::convertible_to<bool>;
};

template <Equal T>
bool areEqual(T a, T b) {
return a == b;
}

template <Ordering T>
T getSmaller(T a, T b) {
return (a < b) ? a : b;
}

int main() {

std::cout << std::boolalpha << std::endl;

std::cout << "areEqual(1, 5): " << areEqual(1, 5) << std::endl;

std::cout << "getSmaller(1, 5): " << getSmaller(1, 5) << std::endl;

std::unordered_set<int> firSet{1, 2, 3, 4, 5};
std::unordered_set<int> secSet{5, 4, 3, 2, 1};

std::cout << "areEqual(firSet, secSet): " << areEqual(firSet, secSet) << std::endl;

// auto smallerSet = getSmaller(firSet, secSet);

std::cout << std::endl;

}

Die Funktion getSmaller fordert, dass beide Argumente a und b das Concept Ordering unterstützen. Darüber hinaus müssen sie denselben Datentyp besitzen. Diese Anforderung gilt natürlich für die Zahlen 1 und 5.

std::unordered_set setzt das Concept Ordering nicht um. Der aktuelle Microsoft-Compiler ist sehr eindeutig, wenn ich die Zeile auto smaller = getSmaller(firSet, secSet) mit dem Flag /std:c++latest übersetze.

Ich möchte hervorheben, dass die Fehlermeldung das Problem sehr gut auf den Punkt bringt: the associated constraints are not satisfied.

Natürlich besitzt C++20 bereits das Concept Ordering:

  • std::three_way_comparable: entspricht meinem Concept Ordering.
  • std::three_way_comparable_with: erlaubt den Vergleich von Werten mit verschiedenen Datentypen; zum Beispiel: 1.0 < 1.0f.

Eventuell verwirrt dich ein wenig der Ausdruck three-way. Mit C++20 erhalten wir den Drei-Wege-Vergleichsoperator; auch bekannt unter dem Namen Spaceship Operator: <=>. Hier gibt es den ersten Überblick: C++20: Überblick zur Kernsprache. Den Operator werde ich in einem zukünftigen Artikel genauer vorstellen.

Ich lerne Neues immer am besten, wenn ich es ausprobiere. Eventuell hast du keinen aktuellen Microsoft-Compiler. In diesem Fall bietet es sich an, den aktuellen GCC (trunk) des Compiler Explorer zu verwenden. GCC unterstützt die C++20-Syntax für Concepts. Hier ist der direkte Link zu dem Programm conceptsDefinitionOrdering.cpp für weitere Experimente: https://godbolt.org/z/uyVFX8.

Wenn du einen konkreten Datentyp definieren willst, der sich einfach im C++-Ökosystem verwenden lässt, sollte er sich "wie ein int verhalten". Solch ein konkreter Typ lässt sich kopieren und das Ergebnis der Copy-Operation ist unabhängig vom ursprünglichen Wert. Beide Objekte besitzen natürlich nach der Copy-Operation den gleichen Wert. Formal gesprochen, sollte ein konkreter Datentyp ein regular type sein. In meinem nächsten Artikel werde ich die Concepts Regular und SemiRegular definieren.

Ich freue mich darauf, weitere C++-Schulungen halten zu dürfen.

Die Details zu meinen C++- und Python-Schulungen gibt es auf www.ModernesCpp.de. ()