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.

In Pocket speichern vorlesen Druckansicht 318 Kommentare lesen

(Bild: Dilok Klaisataporn/Shutterstock.com)

Lesezeit: 14 Min.
Von
  • Andreas Fertig
Inhaltsverzeichnis

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.

C++20-Konzepte

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.

Die verschiedenen Orte, an denen Konzepte verwendet werden können (Abb. 1)

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.