C++20: Die vier großen Neuerungen

Dieser Artikel stellt die vier großen Neuerungen von C++20 im Überblick vor: Concepts, Ranges, Coroutinen und Module.

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

Dieser Artikel stellt die großen vier Neuerungen von C++20 im Überblick vor: Concepts, Ranges, Coroutinen und Module.

Bevor ich dir aber einen ersten Eindruck gebe, möchte ich dir einen Überblick zu C++20 liefern. Der Zeitstrahl unten sollte für den ersten Überblick ausreichen. Neben den großen vier gibt es viele Features, die die Kernsprache, die Bibliothek und Concurrency betreffen.

Die einfachste Art, sich mit den neuen Features vertraut zu machen, ist es, mit ihnen herumzuspielen. Damit stellt sich gleich die Frage: Welche C++20-Features werden durch welchen Compiler unterstützt? Wie so oft gibt dir cppreference.com/compiler_support die aussagekräftige Antwort zur Kernsprache und zur Bibliothek.

Um es einfach zu machen: Die brandneuen GCC-, Clang- und EDG-Compiler bieten die breiteste Unterstützung für die Kernsprache an. Zusätzlich unterstützen der MSVC und der Apple Clang Compiler viele C++20-Features.

Die Geschichte zur Bibliothek ist ähnlich. GCC besitzt die breiteste Unterstützung für die Bibliothek, gefolgt vom Clang-Compiler und dem MSVC.

Die Screenshots zeigen dir nur den Anfang der Tabellen. Dies ist leider eine Antwort, die nicht zufrieden stellen kann. Selbst wenn du alle aktuellen Compiler einsetzt, gibt es viele Features, die noch keine Unterstützung durch irgendeinen Compiler besitzen.

Oft gibt es aber Workarounds, um die neuen Feature auszuprobieren. Hier sind zwei Beispiele:

  • Concepts: GCC unterstützt eine vorherige Version der Concepts.
  • std::jthread: Es gibt eine Entwurfsimplementierung auf GitHub von Nicolai Josuttis.

Um meine Geschichte abzukürzen: Die Unterstützung der C++20-Features ist nicht so dünn. Mit ein wenig Bastelarbeit lassen sich viele neue Features ausprobieren. Wenn notwendig, werde ich auf die Bastelarbeit eingehen.

Nun möchte ich aber mit einem Blick aus der Vogelperspektive auf die neuen Feature beginnen.

Die zentrale Idee der generischen Programmierung mit Templates ist es, Funktionen und Klassen zu definieren, die sich mit verschiedenen Datentypen verwenden lassen. Oft passiert es aber, dass du ein Template mit einem falschen Datentyp instanziierst. Das typische Ergebnis sind ein paar Seiten voll von kryptischen Fehlermeldungen. Diese traurige Geschichte endet mit Concepts. Concepts erlauben es dir, Bedingungen an deine Templates zu stellen, die der Compiler verifiziert. Concepts revolutionieren die Art und Weise, wie wir über generischen Code denken und diesen implementieren. Hier sind Gründe:

  • Die Anforderungen an die Templates sind Bestandteil des Interfaces.
  • Das Überladen von Templates oder die Spezialisierung von Templates ist aufgrund von Concepts möglich.
  • Wir erhalten verbesserte Fehlermeldungen, denn der Compiler kann die Bedingungen an die Template-Parameter mit den Template-Argumenten vergleichen.

Das ist aber noch nicht das Ende der Geschichte.

  • Du kannst vordefinierte Concepts verwenden oder deine eigenen Concepts definieren.
  • Die Verwendung von auto und Concepts wird vereinheitlicht. Daher lässt sich anstelle von auto einfach ein Concept verwenden.
  • Wenn eine Funktionsdeklaration ein Concept verwendet, wird die Funktion automatisch zum Funktions-Template. Das Schreiben von Funktions-Templates wird damit so einfach wie das Schreiben von Funktionen.

Das folgende Codebeispiel zeigt die Definition und die Anwendung des Concept Integral:

template<typename T>
concept bool Integral(){
return std::is_integral<T>::value;
}

Integral auto gcd(Integral auto a,
Integral auto b){
if( b == 0 ) return a;
else return gcd(b, a % b);
}

Integral ist ein Concept, das von seinem Type-Parameter fordert, dass std::is_integral<T>::value true ergibt. std::is_integral<T>::value ist eine Funktion aus der Type-Traits Bibliothek, die ihre Prüfung genau zur Compilezeit ausführt. Wenn std::is_integral<T>::value true ergibt, ist alles gut. Falls nicht, erhältst du einen Fehler zur Compilezeit. Für meine neugierigen Leser – du solltest neugierig sein – sind hier meine Artikel zur Type-Traits Bibliothek.

Der gcd-Algorithmus bestimmt den größten gemeinen Teiler zweiter Zahlen, basierend auf dem Euklid-Algorithmus. Ich habe die sogenannte abbreviated function template syntax verwendet, um gcd zu definieren. gcd fordert von seinen Argumenten und seinem Rückgabewert, das diese das Concept Integral unterstützen. gcd ist eine Art Funktions-Template, das Bedingungen an seine Argumente und seinen Rückgabewert stellt. Wenn ich den Syntactic Sugar entferne, siehst du die wahre Natur der gcd-Funktion.

template<typename T>
requires Integral<T>()
T gcd(T a, T b){
if( b == 0 ) return a;
else return gcd(b, a % b);
}

Wenn du die wahre Natur der gcd-Funktion nicht siehst, muss ich dich auf meine Artikel zu Concepts vertrösten, die ich in ein paar Wochen verfassen werde.

Die Ranges Bibliothek ist der erste Kunde von Concepts. Sie bietet Algorithmen an, die

  • auf den ganzen Container arbeiten. Daher musst du keinen Bereich mehr mittels Iteratoren angeben.
  • lazy evaluiert werden.
  • komponiert werden können.

Um es kurz zu machen, die Ranges-Bibliothek bietet funktionale Pattern an.

Okay, Code sagt mehr als Worte. Das folgende Beispiel zeigt die Funktionskomposition mit dem Pipe-Symbol

#include <vector>
#include <ranges>
#include <iostream>

int main(){
std::vector<int> ints{0, 1, 2, 3, 4, 5};
auto even = [](int i){ return 0 == i % 2; };
auto square = [](int i) { return i * i; };

for (int i : ints | std::view::filter(even) |
std::view::transform(square)) {
std::cout << i << ' '; // 0 4 16
}
}

even ist eine Lambda-Funktion, die ermittelt, ob i gerade ist. Die Lambda-Funktion square bildet ihr Argument auf das Quadrat ab. Der Rest des Codes ist Funktionskomposition, die du von links nach rechts lesen musst: for (int i : ints | std::view::filter(even) | std::view::transform(square)). Wende auf jedes Argument von ints den Filter even an und bilde die verbleibenden Argumente auf ihr Quadrat ab. Wenn du mit funktionaler Programmierung vertraut bist, liest sich dies wie Prosa.

Couroutinen sind verallgemeinerte Funktionen, die ihre Ausführung anhalten und wieder aufnehmen können. Dabei behalten sie ihren Zustand. Coroutinen stellen die typische Art dar, Event-getriebenen Applikationen zu schreiben. Event-getriebenen Applikationen können Simulationen, Spiele, Server, Benutzerinterface und selbst Algorithmen sein. Couroutinen werden auch gerne für kooperatives Multitasking verwendet.

Wir bekommen mit C++20 keine konkreten Coroutinen, sondern eine Framework für das Schreiben von Coroutinen. Dieses Framework besteht aus mehr als 20 Funktionen, die teilweise implementiert werden müssen oder können. Daher lässt sich das Verhalten von Coroutinen auf seine eigenen Anforderungen genau zuschneiden.

Der Verständlichkeit halber möchte ich eine einfache Coroutine zeigen. Das folgende Programm verwendet ein Erzeuger für einen unendlichen Datenstrom:

Generator<int> getNext(int start = 0, int step = 1){
auto value = start;
for (int i = 0;; ++i){
co_yield value; // 1
value += step;
}
}

int main() {

std::cout << std::endl;

std::cout << "getNext():";
auto gen = getNext();
for (int i = 0; i <= 10; ++i) {
gen.next(); // 2
std::cout << " " << gen.getValue();
}

std::cout << "\n\n";

std::cout << "getNext(100, -10):";
auto gen2 = getNext(100, -10);
for (int i = 0; i <= 20; ++i) {
gen2.next(); // 3
std::cout << " " << gen2.getValue();
}

std::cout << std::endl;

}

Dazu benötigst du eine paar Erläuterungen. Dies ist nur eine Codeschnipsel. Die Funktion getNext ist eine Coroutine, da sie das Schlüsselwort co_yield verwendet. getNext besitzt eine Endlosschleife, die den Wert nach co_yield zurückgibt. Ein Aufruf next() in den Zeilen 2 und 3 weckt die Coroutine auf, der anschließende getValue-Aufruf erlaubt es, den Wert abzuholen. Nach dem getNext-Aufruf pausiert die Coroutine wieder. Sie pausiert bis zum nächsten next()-Aufruf. Natürlich gibt es noch eine große Unbekannte in meinem Beispiel. Diese Unbekannte ist der Rückgabewert Generator<int> des getNext-Funktion. Hier starten die komplizierten Aspekte von Coroutinen, die ich in weiteren Artikel genau erkläre.

Dank dem Wandbox Online Compiler, kann ich die Ausgabe des Programms zeigen.

Zu Modulen halte ich mich jetzt sehr kurz, da mein Artikel bereits zu lange ist.

Module versprechen:

  • schnellere Compilierungszeiten
  • Isolation von Makros
  • die logische Struktur des Codes auszudrücken
  • Headerdateien überflüssig zu machen
  • eklige Makro-Tricks zu vermeiden

Nach dem Überblick zu den großen vier werde ich mir in meinem nächsten Artikel die Neuerungen der Kernsprache genauer anschauen. ()