C++20-Konzepte: Neue Wege mit Konzepten
Konzepte sind eine Neuerung in C++20, die zu verständlicheren Fehlermeldungen und besser lesbarem Code verhelfen können, wie dieser Artikel zeigt.
- Andreas Fertig
Eine Stärke von C++ ist es, generischen Code schreiben zu können. Ein Algorithmus wird generisch implementiert und ist mit verschiedenen Datentypen verwendbar, die verschiedene Anforderungen erfüllen müssen. Konzepte ermöglichen es erstmals, mit Sprachmitteln Anforderungen an generische Datentypen zu formulieren. Das verleiht dem Code mehr Ausdruckskraft. Dieser Teil der Artikelserie zeigt, wie sich Code durch Konzepte klarer formulieren lässt. Bisher waren gerade Fehlermeldungen bei Templates gefürchtet. An dieser Stelle kommen Konzepte ins Spiel: Sie sind dazu in der Lage, Fehlermeldungen auf den Punkt zu bringen.
Eingeschränkte Platzhalterdatentypen
Platzhalterdatentypen sind Datentypen, die der Compiler durch auto
selbstständig ermittelt. Dafür verwendet er den bei der Initialisierung zugewiesenen Wert. Die Vorteile zeigen sich bei auto
-Variablen. Sie sind dann interessant, wenn der genaue Datentyp nicht näher spezifiziert werden soll.
Im nachfolgenden Codeausschnitt stellt static_assert
sicher, dass x
ein integraler Datentyp ist. Das ist ein häufiges Muster in generischem Code. Ob der Datentyp signed
oder unsigned
ist und ob es sich um einen long int
oder nur um einen int
handelt, spielt für den Algorithmus und die Berechnung keine Rolle. Ausgeschlossen sind dagegen Gleitkommadatentypen wie float
und double
.
Ohne C++20 existierte nur die Option, mit einem Type Trait std::is_integral
und einem static_assert
zu arbeiten:
template<typename T>
auto SomeFunction(T& value)
{
// ...
auto x = Calculate(value);
static_assert(std::is_integral_v<decltype(x)>,
"Only integrals are allowed");
// ...
}
Die Umsetzung funktioniert, ist aber nicht elegant, allein schon durch den Einsatz von decltype
, das wiederum x
als eine unnötige Wiederholung erfordert. Schließlich ist die zusätzliche Anweisung static_assert
mit oder ohne Fehlermeldung eine Zeile Code mehr. Zusätzlich besteht das Risiko, bei mehr als einer Variablen die falsche zu testen.
All diese Fragen und potenziellen Fehlerquellen entfallen dank eingeschränkter auto
-Variablen in C++20. Zunächst ist jedoch zu beachten, dass an dieser Stelle nur Konzepte verwendet werden können. Praktischerweise enthält die Standardbibliothek das Konzept std::integral
. Damit lässt sich die Einschränkung von x
wie folgt formulieren:
template<typename T>
auto SomeFunction(T& value)
{
// ...
std::integral auto x = Calculate(value);
// ...
}
Im Vergleich zum Overhead und den Entscheidungen, die es in der C++17-Variante zu treffen gilt, ist das Arbeiten mit der C++20-Version wesentlich angenehmer. Einfach und kompakt erlaubt sie das Formulieren der Erwartungshaltung an den Datentyp in generischem Code.
Abgekürzte Funktions-Templates
Die Constraint-Platzhaltertypen C3
und C4
, die auto
-Datentypen einschränken, sind bereits aus dem ersten Teil bekannt. Ein konkreter Fall sind die in C++20 neu hinzugekommenen auto
-Parameter. Setzt eine Funktion sie ein, handelt es sich um ein abgekürztes Funktions-Template.
Das folgende Listing zeigt beispielhaft ein vom Autor gerne eingesetztes Muster: Ein Funktions-Template fordert zuerst einen globalen Mutex an, um danach eine dann geschützte Funktion aufzurufen. Der Vorteil dieses Patterns ist, dass der Name des globalen Mutex nur innerhalb der Funktion bekannt sein muss, statt überall in der Codebasis verstreut zu sein.
template<typename T>
void DoLocked(T&& f)
{
std::lock_guard lock{globalOsMutex};
f();
}
Zwei Dinge sind an DoLocked
verbesserungswürdig: Erstens erfordert die grundsätzlich einfache Funktion viel Schreibarbeit, wobei vor allem der Template-Kopf störend wirkt. Weitaus schwerwiegender ist der zweite Aspekt: Für Benutzer ist es intransparent, dass DoLocked
durch typename
ein ausführbares Objekt als Parameter erwartet. Zwar teilt der Compiler das in der Fehlermeldung mit, aber meist erst nach gefühlten 20 Seiten. Das folgende Listing zeigt, wie beides in C++20 besser geht:
void DoLocked(std::invocable auto&& f)
{
std::lock_guard lock{globalOsMutex};
f();
}
Bei einem abgekürzten Funktions-Template entfällt der Template-Kopf. Wie der Name schon sagt, bedeutet das gleichzeitig, dass jeder auto
-Parameter eine Funktion in ein Funktions-Template wandelt.
Wichtig ist aus Sicht des Autors, dass das Konzept std::invocable
Nutzern der Funktion DoLocked
kommuniziert, dass f
aufrufbar sein muss. Ohne das abgekürzte Funktions-Template ist der Einsatz des Konzepts anstelle von typename
eine Alternative in C++20. Der auto
-Parameter und das Konzept ermöglichen die Einschränkung und Reduzierung von zu lesendem und zu schreibendem Code.