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.
- Rainer Grimm
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;
}
Meine falsche Anwendung des Concepts Equal
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 fi
r
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>;
};
s
td::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 ConceptEqual.
std::equality_comparable_with
: erlaubt den Vergleich von Werten mit verschiedenen Datentypen; zum Beispiel:1.0 == 1.0f.
Die Herausforderung
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
.
Das Concept Ordering
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: t
he associated constraints are not satisfied
.
Natürlich besitzt C++20 bereits das Concept Ordering
:
-
std::three_way_comparable
: entspricht meinem ConceptOrdering.
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.
Compiler-Unterstützung
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.
Wie geht's weiter?
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.
C++-Schulungen im Großraum Stuttgart
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. ()