C++20 und seine Neuerungen

Der C++20-Standard wurde im Dezember 2020 finalisiert. Die großen Compiler (GCC, Clang, MSVC, …) und deren Standardbibliotheken unterstützen ihn mittlerweile weitgehend. Vereinzelt sind jedoch noch Lücken vorhanden (siehe compiler_support#cpp20). Aber welche Neuerungen bringt der Standard eigentlich mit?

Neue Sprachfeatures Update; C++20 Neuerungen

C++20 führt eine ganze Reihe neuer Features ein. Eine vollständige Übersicht findet sich auf cppreference.comMeine persönlichen Highlights sind:

  • Feature test macros: Damit lässt sich prüfen, welche C++ Sprachfeatures der Compiler unterstützt. Dadurch kann man lokale Anpassungen am Quellcode vornehmen, um unterschiedliche Entwicklungsumgebungen (z.B. verschiedene Compiler-Versionen) zu unterstützen.
  • operator<=>: Dies ist ein 3-Wege Vergleichsoperator, d.h. er liefert kleiner, gleich oder größer als Ergebnis zurück. Ist dieser Operator implementiert, generiert der Compiler die übrigen Vergleichsoperatoren automatisch. Dies spart Zeit und Schreibarbeit.
  • Modules: Module bieten eine weitere Möglichkeit den Quellcode zu unterteilen. Im Gegensatz zu includes wird beim Import eines Moduls nicht der komplette Quellcode in die Zieldatei eingefügt. Stattdessen werden nur explizit exportierte Funktionen und Klassen verfügbar gemacht. Module sind zudem in sich geschlossene Einheiten. Makrodefinitionen, usings und warning supressions sind daher nur innerhalb eines Moduls gültig. Dies verhindert schwer zu findende Probleme wie z.B., dass in den Modulen definierte Makros Funktionen oder Konstanten im eigenen Code überschreiben.
  • Coroutines: Dies sind kooperative Funktionen, die an geeigneten Stellen ihre Ausführung unterbrechen und temporär die Kontrolle an andere Coroutines transferieren. Damit lassen sich z.B. asynchrone Funktionen oder eine lazy evaluation (eine Auswertung, die erst bei Bedarf erfolgt) mit geringem Aufwand implementieren.
  • Constraints und concepts: Diese ermöglichen es, template Funktionen auf bestimmte Typen einzuschränken. Zudem liefert der Compiler eine eindeutige Fehlermeldung, falls nicht unterstütze Typen verwendet werden. Im Kontrast dazu, sind die üblichen Compiler-Fehler aus template-basiertem Code gerade für C++ Neulinge schwer zu verstehen; diese Änderung schafft Abhilfe.

Designated initializers & abbreviated function templates

Weiterhin gibt es noch einige kleinere Erweiterungen. Diese ermöglichen es vor allem einfacheren und besser lesbaren Code zu schreiben. Ein Beispiel sind designated initializers. Mit deren Hilfe können einzelne Member initialisiert werden, ohne dass explizit alle Member initialisiert werden müssen:

struct ComplexConfig { int a = 1; int b = 2; int c = 3; };

const ComplexConfig configB{ 1, 4, 3 }; //init every member explicitely
ComplexConfig configA{}; configA.b = 4; //overwrite b after default initialization

const ComplexConfig configC{ .b = 4 }; //using a designated initializer

Ein anderes Beispiel sind abbreviated function templates:

template<typename T> concept Bird = requires(T a) {
    { a.fly() }; //This is a constraint, i.e. it has to compile for every T used as Bird
};

template<class X, Bird Y> void doSomething(X x, Y b) {}; //function template
void doSomething(auto x, Bird auto y); //abbreviated function template

Die zweite Deklaration von doSomething ist identisch zur ersten Deklaration, jedoch ist sie deutlich kompakter. Beide Varianten unterstützen concepts. Daher wird doSomething nur kompilieren, falls der konkrete Typ für Y eine Funktion fly implementiert.

Erweiterungen der Standardbibliothek mit C++20

Die Standardbibliothek wurde mit C++20 ebenfalls erweitert. Besonders interessant finde ich ranges, die eine Alternative zu for-Schleifen bieten. Nehmen wir an, dass wir aus einer Liste von ganzen Zahlen die Summe der Quadrate der ersten N Werte größer als 50 bilden wollen. Der Code dafür könnte folgendermaßen aussehen:

int sumOfSquaresForFirstNValuesGreater50(const std::vector& input, int n) {
    int sum = 0, count = 0;
    for (const auto& v : input) {
        if (greaterThan50(v)) {
            sum += square(v);
            if (++count >= n) {
                break;
            }
        }
    }
    return sum;
}

 Abstrahiert betrachtet brauchen wir eine Filterung, eine Transformation und eine Reduktion unserer Werte. Hier ist die gleiche Aufgabe mit Hilfe von ranges gelöst:

int sumOfSquaresForFirstNValuesGreater50UsingRanges(const std::vector& input, int n) {
    using namespace std::views;
    auto squares = input
        | filter(greaterThan50)
        | take(n)
        | transform(square) //could be merged with the reduce
        | common; //only needed to pass the result to reduce, which does not support ranges
    return std::reduce(std::begin(squares), std::end(squares), 0);
}

Ein Vorteil der Variante mit ranges ist ein höheres Abstraktionslevel und damit eine bessere Lesbarkeit (insbesondere bei komplexen Problemen). Die Abstraktion ist leider nicht ganz ohne Nachteile: Die Funktion greaterThan50 wird je nach Input häufiger aufgerufen als in der ersten Variante. Dies kann zu einer schlechteren Performance führen. 

All diese Beispiele zeigen, wie sich C++ stetig weiterentwickelt. Man darf gespannt sein, welche Neuerungen die nächste Version (C++23) mit sich bringt. 

 

Mehr Informationen zum Thema Software Engineering findest du hier in unserem Kompetenzfeld.

Alexander Neuhöfer
Letzte Artikel von Alexander Neuhöfer (Alle anzeigen)