Concepts in C++20 revolutionieren die Art und Weise, wie wir generischen Code verstehen und schreiben. Sie erfüllen eine ähnlich fundamentale Aufgabe wie Interfaces für die objektorientierte Programmierung.
Revolutionen beginnen oft mit kleinen Problemen. Die Klasse »Account« in Listing 1 ist ein einfaches Konto. Es bietet neben einem Default-Konstruktor eine Variante an, bei der sich der Kontostand »balance« explizit setzen lässt.
Listing 1
Klasse Account
class Account {
public:
Account() = default;
Account(double bal): balance{bal} {}
private:
double balance{0.0};
};
Die ersten Kundenwünsche lassen nicht lange auf sich warten: Es sollen sich doch bitte zwei Konten vergleichen lassen. Als Grundlage des Vergleichs soll der Kontostand dienen. Zwar lässt sich eine generische Vergleichsfunktion »isSmaller()« schnell implementieren, doch führt der Vergleich aus Listing 2 keineswegs zum gewünschten Ergebnis: Für die Klasse »Account« ist der Kleiner-Operator schlicht nicht definiert. Im Gegensatz zu »double«-Werten (Zeile 20) lassen sich Konten (Zeile 23) nicht vergleichen. Das moniert der Compiler in Abbildung 1 mit einer deutlichen Fehlermeldung.
Listing 2
Kontovergleich
#include <iostream>
class Account {
public:
Account() = default;
Account(double bal): balance{bal} {}
private:
double balance{0.0};
};
template <typename T>
bool isSmaller(T t, T t2) {
return t < t2;
}
int main() {
std::cout << std::boolalpha;
double doub1{};
double doub2{10.5};
std::cout << "isSmaller(doub1, doub2): " << isSmaller(doub1, doub2) << '\n';
Account acc1;
Account acc2(10.5);
std::cout << "isSmaller(acc1, acc2): " << isSmaller(acc1, acc2) << '\n';
}
Die Lösung?
Die Lösung des Problems scheint auf der Hand zu liegen. Der Algorithmus »isSmaller()« in Listing 2 ist viel zu generisch. Die Lösung dieses Problems bieten Concepts. Schnell lässt sich das Concept »Smaller« definieren (Listing 3) und in dem generischen Algorithmus anwenden.
Listing 3
Concept Smaller
template <typename T>
concept Smaller = requires(T a, T b) {
{ a < b } -> std::convertible_to<bool>;
};
template <Smaller T>
bool isSmaller(T t, T t2) {
return t < t2;
}
Das Concept »Smaller« fordert von zwei Variablen »a« und »b« desselben Typs »T«, dass der Kleiner-Operator definiert ist und einen Wert zurückgibt, der sich zu »bool« konvertieren lässt. Beim Ausdruck »std::convertible_to<bool>« handelt es sich selbst ein Concept.
Um das neue Concept anzuwenden, genügt es, im Algorithmus »isSmaller()« in Listing 3 den Ausdruck »typename« durch das Concept »Smaller« zu ersetzen. Ein Concept ist ein Compile-Time-Prädikat, also eine Funktion, die sich zur Übersetzungszeit ausführen lässt und einen Wahrheitswert zurückgibt.
Jetzt müssen wir nur noch den Datentyp »Account« mit dem Kleiner-Operator erweitern, und schon lässt sich das Concept in »account2.cpp« (Listing 4) anwenden. Abbildung 2 zeigt das Programm in Aktion.
Listing 4
Smaller anwenden
#include <concepts>
#include <iostream>
template <typename T>
concept Smaller = requires(T a, T b) {
{ a < b } -> std::convertible_to<bool>;
};
class Account {
public:
Account() = default;
Account(double bal): balance{bal} {}
private:
double balance{0.0};
};
template <Smaller T>
bool isSmaller(T t, T t2) {
return t < t2;
}
int main() {
std::cout << std::boolalpha;
double doub1{};
double doub2{10.5};
std::cout << "isSmaller(doub1, doub2): " << isSmaller(doub1, doub2) << '\n';
Account acc1;
Account acc2(10.5);
std::cout << "isSmaller(acc1, acc2): " << isSmaller(acc1, acc2) << '\n';
}
Nun gilt es, noch ein wenig innezuhalten. Welche Vorteile besitzen Concepts eigentlich? Sie erlauben, Anforderungen an die Template-Parameter zu formulieren, und erzeugen deutlich besser verständliche Fehlermeldungen. Concepts lassen sich als Platzhalter für generischen Code verwenden und für Klassen- und Funktions-Templates sowie Mitgliedsfunktionen von Klassen einsetzen. Darüber hinaus unterstützen sie das Überladen von Funktionen und das Spezialisieren von Klassen-Templates.
Weiter im Text
Allerdings stellt das Programm »account2.cpp« nur vermeintlich die Lösung dar. Ein paar Tage später wünscht sich der Kunde, mit dem Datentyp »Account« auch einen Größer-Vergleich anstellen zu können.
Wieder ist die Abhilfe schnell implementiert: Das Programm »account3.cpp« in Listing 5 erweitert »account2.cpp« um das Concept »Greater«, das das Funktions-Template »isGreater« anwendet. Darüber hinaus bietet die Klasse »Account« den Größer-Operator an. Die Ausgabe des Programms (Abbildung 3) zeigt, dass sich das Programm erwartungsgemäß verhält.
Listing 5
Smaller und Greater
#include <concepts>
#include <iostream>
template <typename T>
concept Smaller = requires(T a, T b) {
{ a < b } -> std::convertible_to<bool>;
};
template <typename T>
concept Greater = requires(T a, T b) {
{ a > b } -> std::convertible_to<bool>;
};
class Account {
public:
Account() = default;
Account(double bal): balance{bal} {}
bool operator < (const Account& oth) const {
return balance < oth.balance;
}
bool operator > (const Account& oth) const {
return balance > oth.balance;
}
private:
double balance{0.0};
};
template <Smaller T>
bool isSmaller(T t, T t2) {
return t < t2;
}
template <Greater T>
bool isGreater(T t, T t2) {
return t > t2;
}
int main() {
std::cout << std::boolalpha;
double doub1{};
double doub2{10.5};
std::cout << "isSmaller(doub1, doub2): " << isSmaller(doub1, doub2) << '\n';
std::cout << "isGreater(doub1, doub2): " << isGreater(doub1, doub2) << '\n';
Account acc1;
Account acc2(10.5);
std::cout << "isSmaller(acc1, acc2): " << isSmaller(acc1, acc2) << '\n';
std::cout << "isGreater(acc1, acc2): " << isGreater(acc1, acc2) << '\n';
}
Haben wir damit alle Kundenanforderung umgesetzt? Ja, allerdings nur kurzfristig. Die Kunde wird relativ schnell wünschen, dass der Datentyp »Account« auch die vier verbleibenden Vergleichsoperatoren »=«,»!O«, »<=« und »>=« unterstützt. Das zwingt uns, ein paar Schritte zurück zu machen und eine formalere Analyse der Anforderungen anzustellen.
Typklassen
Concepts sollten semantische Kategorien modellieren, nicht syntaktische Einschränkungen. Die zwei Concepts »Smaller« und »Greater« prüfen aber nur, ob ein konkreter Datentyp den Kleiner- oder Größer-Operator implementiert.
Die Typklassen [1] von Haskell ähneln C++-Concepts weitgehend. Die Sprache ist nach dem Mathematiker Haskell Curry [2] benannt. Da bietet es sich förmlich an, die Umsetzung von Ordnung in Haskell genauer zu studieren. Abbildung 4 bietet einen Überblick über die wesentlichen Typklassen von Haskell. Für die Typklassen »Eq« und »Ord« zeigt Listing 6 sehr schön, was das bedeutet.
Listing 6
Eq und Ord
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
Die Typklasse Ordnung »Ord« setzt die Typklasse Gleichheit (»Eq«) voraus. Die Typklasse »Eq« wiederum verlangt von zwei Werten »a« mit demselben Typ, dass die Operationen »==« und »/=« definiert sind und einen Datentyp »Bool« zurückgeben.
Die Typklasse »Ord« verfeinert die Typklasse »Eq«. »Ord« verlangt zusätzlich, dass die Funktionen »compare«, »max« und die vier verbleibenden Operatoren kleiner »<«, kleiner gleich »<=«, größer »>« und größer gleich »>=« definiert sind. Das lässt sich in C++ fast direkt mit Concepts ausdrücken. Der Einfachheit halber verzichten wir bei der Implementierung des C++-Concepts »Ordering« in Listing 7 auf die zwei Haskell-Funktionen »compare« und »max«.
Listing 7
Equal und Ordering
#include <concepts>
#include <iostream>
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>;
};
class Account {
public:
Account() = default;
Account(double bal): balance{bal} {}
auto operator <=> (const Account& oth) const = default;
private:
double balance{0.0};
};
template <Ordering T>
bool isSmaller(T t, T t2) {
return t < t2;
}
template <Ordering T>
bool isGreater(T t, T t2) {
return t > t2;
}
int main() {
std::cout << std::boolalpha;
double doub1{};
double doub2{10.5};
std::cout << "isSmaller(doub1, doub2): " << isSmaller(doub1, doub2) << '\n';
std::cout << "isGreater(doub1, doub2): " << isGreater(doub1, doub2) << '\n';
Account acc1;
Account acc2(10.5);
std::cout << "isSmaller(acc1, acc2): " << isSmaller(acc1, acc2) << '\n';
std::cout << "isGreater(acc1, acc2): " << isGreater(acc1, acc2) << '\n';
}
Die Funktions-Templates »isSmaller« (Zeile 31) und »isGreater« (Zeile 36) wenden direkt das Concept »Ordering« an, das die Zeilen 4 bis 19 zusammen mit dem Concept »Equal« definieren.
Ein weiteres C++20-Feature kommt in der Klasse »Account« zum Einsatz: Zeile 25 fordert den sogenannten Drei-Wege-Vergleichsoperator an. Er erzeugt automatisch alle sechs Vergleichsoperatoren. Dabei delegiert die Vergleichsoperationen an alle Member und alle Basisklassen (sofern vorhanden). Die Ausgabe des Programms zeigt Abbildung 5.
Wie geht es weiter?
Nach diesem einführenden Beitrag zu Concepts tauchen wir mit dem nächsten Artikel weiter in die Tiefe ein. Daneben werden in der nächsten Folge Themen wie das Überladen von Funktionen, die Spezialisierung von Templates und die Definition von Concepts zur Sprache kommen. (jlu)
Infos
- Haskell-Typklassen: https://de.wikipedia.org/wiki/Typklasse_(Informatik)
- Haskell Curry: https://en.wikipedia.org/wiki/Haskell_Curry











