C++17 – Was gibt’s Neues? Ein Überblick über die wichtigsten Erweiterungen

Autor / Redakteur: Rainer Grimm * / Sebastian Gerstl

Ende 2017 wurde es vollbracht: C++17 ist als neuer ISO-Standard einstimmig verabschiedet. Da stellt sich natürlich die Frage, was gibt es denn an konkreten Neuerungen in C++17? Und wir wirkt sich das für Softwareentwickler aus?

Firma zum Thema

Bild 1: Der Zeitstrahl demonstriert, wie sowohl die C++-Kernsprache als auch die C++-Standardbibliothek deutlich mächtiger geworden sind.
Bild 1: Der Zeitstrahl demonstriert, wie sowohl die C++-Kernsprache als auch die C++-Standardbibliothek deutlich mächtiger geworden sind.
(Bild: Rainer Grimm)

Bevor der hier vorliegende Artikel nun den Blick auf die Details zu C++17 schärft, ist es zuerst notwendig, das große Bild im Auge zu behalten. C++17 setzt die Serie von neuen C++-Standards im Drei-Jahres-Rhythmus konsequent um (siehe Bild 1). Der Zeitstrahl zeigt bereits: Sowohl die C++-Kernsprache als auch die C++-Standardbibliothek sind deutlich mächtiger geworden.

Bildergalerie
Bildergalerie mit 5 Bildern

Die verbesserte Kernsprache

Fold expressions, constexpr if, Initialisierer in if und switch Anweisungen, structured binding Deklarationen, automatische Typbestimmung für Konstruktoren von Templates, Vermeindung von Kopieren; die Liste an neuen Features in C++ ist lang. Sowohl für den Anwender als auch für den Bibliotheksautoren in C++ sind viele Perlen dabei.

Fold Expressions

C++11 unterstützt Variadic Templates. Dies sind Templates, die eine beliebige Anzahl an Argumenten annehmen können. Die beliebige Anzahl wird von einem Parameter-Pack gehalten.

Neu ist mit C++17, dass dieses Parameter-Pack direkt mit einem binären Operator zur Übersetzungszeit reduziert werden kann.

#include <iostream>

template<typename... Args>
bool all(Args... args) { return (... && args); }

int main(){

    std::cout << std::boolalpha;
    std::cout << "all(): " << all() << std::endl;
    std::cout << "all(true): " << all(true) << std::endl;
    std::cout << "all(true, true, true, false): " << all(true, true, true, false) << std::endl;

    std::cout << std::endl;

}

Der binäre Operator des Funktions-Templates all ist das logische UND (&&). Hier ist die Ausgabe des Programms:

Output:

all(): true
all(true): true
all(true, true, true, false): false

Es geht mit den Features zur Übersetzungszeit weiter.

Constexpr if

constexpr if erlaubt es, Sourcecode bedingt zu übersetzten.

template <typename T>
auto get_value(T t){
     if constexpr (std::is_pointer_v)    //(1)
        return *t; // deduces return type to int for T = int*
    else     //(2)
return t;    // deduces return type to int for T = int
}

Falls T ein Zeiger ist, dann wird der if Zweig (1) der Funktion get_value übersetzt, falls nicht, der else Zweig (2). Zwei Punkte sind in diesem Zusammenhang wichtig. Die Funktion get_value besitzt zwei verschiedene Rückgabetypen und beide Zweige der if-Anweisungen müssen gültig sein.

Konsequenterweise können if und switch Anweisungen in C++17 das, was for Anweisungen schon lange konnten.

Initialisierer in 'if' und 'switch' Anweisungen

Variablen können in C++17 direkt in if und switch Anweisungen initialisiert werden.

std::map<int,std::string> myMap;

if (auto result = myMap.insert(value); result.second){
/     useResult(result.first);
    // ...
}
else{
    //...
} // result is automatically destroyed

Die Variable result ist nur innerhalb des if und else Zweigs der if-Anweisung gültig. result verschmutzt daher nicht den umgebenden Bereich.

Falls der Initialisierer in if und switch Anweisungen zusammen mit der structured binding Deklaration zum Einsatz kommt, wird die C++ Syntax noch eleganter.

Structurted binding Deklarationen

Dank structured binding lässt sich direkt ein std::tuple oder ein struct an eine Variable binden. Damit wird das vorherige Beispiel deutlich lesbarer.

std::map<int,std::string> myMap;

if (auto [iter, succeeded] = myMap.insert(value); succeeded) {// (1)
    useIter(iter);
    // ...
}
else{
    //...
} //iter and succeded are automatically be destroyed

auto [iter, succeeded] (1) erzeugt automatisch die zwei Variablen iter und succeeded. In der letzten Zeile werden sie wieder gelöscht.

Das ist ein Feature in C++17, mit dem Programmieren leichter von der Hand wird. Das gleiche gilt für die automatische Template-Typ Ableitung von Konstruktoren.

Automatische Typbestimmung für Konstruktoren von Templates

Bild 2: Für die auskommentierten, Konstruktoraufrufe ShowMe ohne eckigen Klammern ist ein C++17 Compiler notwendig.
Bild 2: Für die auskommentierten, Konstruktoraufrufe ShowMe ohne eckigen Klammern ist ein C++17 Compiler notwendig.
(Bild: Rainer Grimm)

Ein Funktions-Template kann seine Typ-Parameter von seinen Funktions-Argumenten ableiten. Aber das war für ein spezielles Funktions-Template nicht möglich: Den Konstruktor eines Klassen-Templates. Mit C++17 ist dieser Satz falsch. Der Konstruktor kann seine Typ-Parameter von seinen Konstruktor-Argumenten ableiten.

Die showMe Aufrufe in der main-Funktion des in Bild 2 gezeigten Beispiels konnte bereits der erste C++98-Standard übersetzen. Für die auskommentierten, anschließenden Konstruktoraufrufe ShowMe ohne eckigen Klammern ist ein C++17 Compiler notwendig. Mit diesem müssen keine eckigen Klammern mehr verwendet werden, um ein Klassen-Template zu instanziieren.

Bildergalerie
Bildergalerie mit 5 Bildern

Aber in C++17 geht es nicht nur um die Benutzerfreundlichkeit. Es geht auch in bekannter Manier um Performanz der Anwendung.

Vermeidung von Kopieren

RVO steht für Return Value Optimization und bedeutet, dass der Compiler unnötige Copy-Operation entfernen kann. Was bisher ein optionaler Optimierungsschritt war, muss der Compiler in C++17 verbindlich zusichern.

MyType func() {
    return MyType{};    // no copy with C++17
}
MyType myType = func();     // no copy with C++17

Zwei unnötige Copy-Operationen können in den paar Zeilen stattfinden. Die erste in der return Anweisung und die zweite in der Zuweisung des Ergebnisses. Mit C++17 darf das nicht mehr passieren.

Falls ein Feature nicht mehr notwendig oder seine Anwendung sehr fehleranfällig ist, sollte es entfernt werden. Genau das passiert in C++17 mit std::auto_ptr und Trigraphen.

std::auto_ptr und Trigraphen entfernt

std::auto_ptr ist der erste Smart Pointer in C++. Sein Job ist es, genau auf seine Ressource aufzupassen. Aber er besitzt ein großes Problem. Wenn ein std:auto_ptr kopiert wird, findet heimlich eine Move-Operation statt. Das ist der Grund, dass C++11 std::unique_ptr als Ersatz erhielt. Ein std::unique_ptr kann nicht kopiert werden.

Trigraphen sind drei Zeichen im Sourcecode, die wie ein Zeichen behandelt werden. Sie sind dann notwendig, wenn die Tastatur das gewünschte Zeichen nicht unterstützt.

Falls es ihr Ziel ist, unleserlichen Code in C++ zu schreiben, dann ist dies mit C++17 deutlich schwieriger, denn mit C++17 sind die folgenden Zeilen nicht mehr gültig.

Neue Bibliotheken

Ein std::string_view ist eine nicht-besitzende Referenz auf einen String. Diese repräsentiert eine Sequenz von Zeichen. Diese Sequenz von Zeichen kann ein C++-String oder ein C-String sein.

Eine Frage bleibt natürlich bestehen. Warum benötigen wir std::string_view? Warum haben Google, LLVM und Bloomberg bereits eine Implementierung eines String-View? Die Antwort ist einfach. Es ist sehr billig einen std::string_view zu kopieren. std::string_view benötigt nur zwei Informationen: einen Zeiger auf die Sequenz von Zeichen und deren Länge. Wie sie vermutlich schon vermuten, besteht std::string_view fast ausschließlich aus lesenden Operationen, die dem Interface des std::string folgen. Fast nur, denn er erhält die zwei neuen Methoden remove_prefix und remove_suffix (siehe Bild 3).

Bild 3: Ein Beispiel zur Verwendung von std::string_view.
Bild 3: Ein Beispiel zur Verwendung von std::string_view.
(Bild: Rainer Grimm)

Das Programm sollte kein großes Überraschungspotential bergen. Die std::string_view's erhalten in (1) und (3) ihre Referenz auf den C++-String und die Sequenz von Zeichen. In Ausdruck (2) werden alle führenden Nicht-Leerzeichen (strView.find_first_not_of(" ")) und in Ausdruck (4) alle abschließenden "\0"-Zeichen (strView2.find("\0")) entfernt.

Die Details zu std::string_view lassen sich schön auf cppreference.com nachlesen und das Programm bereits in Aktion bewundern.

Parallele Algorithmen der Standard Template Library

Bild 4: In C++17 hält die Standard Template Library 69 neue Varianten (schwarz) und acht neue parallele Algorithmen (in rot dargestellt) parat.
Bild 4: In C++17 hält die Standard Template Library 69 neue Varianten (schwarz) und acht neue parallele Algorithmen (in rot dargestellt) parat.
(Bild: Rainer Grimm)

69 Algorithmen der Standard Template Library werden in sequentieller, paralleler und parallel und vektorisierender Variante zu Verfügung stehen. Zusätzlich erhalten wir 8 neue Algorithmen. Die 69 neuen Varianten (schwarz) und die 8 neuen Algorithmen (rot) sind in Bild 4 kurz und kompakt dargestellt:

Welche Version eines Algorithmus ausgewählt wird, steuert der Anwender über das sogenannten Policy Tag. Wie funktioniert das Ganze?

vector v = ...

// standard sequential sort
std::sort(v.begin(), v.end());        // (1)

// sequential execution
std::sort(std::parallel::seq, v.begin(), v.end());    // (2)

// permitting parallel execution
std::sort(std::parallel::par, v.begin(), v.end());    // (3)

// permitting parallel and vectorised execution
std::sort(std::parallel::par_unseq, v.begin(), v.end()); // (4)

Schön ist an dem obigen Beispiel zu sehen, dass die klassische Variante von std::sort (1) mit C++17 immer noch zur Verfügung steht. Dagegen lässt sich jetzt mit C++17 explizit die sequentielle (2), die parallele (3) oder auch die parallele und vektorisierende Variante (4) von std::sort anfordern.

Dateisystem Bibliothek

Die neue Dateisystem Bibliothek basiert auf boost::filesystem. Ihre Komponenten sind optional. Das bedeutet, dass nicht die ganze Funktionalität von std::filesytem auf jeder Plattform verfügbar sein muss. Zum Beispiel unterstützt FAT-32 keine symbolischen Links.

Die Bibliothek enthält die drei Konzepte Datei, Dateinamen und Pfad. Datei können Verzeichnisse, harte Links, symbolische Links oder reguläre Dateien sein. Pfade können absolut oder relativ sein.

Es gibt ein mächtiges Interface für das Lesen und Manipulieren des Dateisystems. Bild 5 zeigt einen ersten Eindruck.

Bild 5: Erster Eindruck des Interfaces für Lesen und Manipulieren der neuen Dateisystem Bibliothek in C++17.
Bild 5: Erster Eindruck des Interfaces für Lesen und Manipulieren der neuen Dateisystem Bibliothek in C++17.
(Bild: Rainer Grimm)

fs::current_path (1) gibt den aktuellen Pfad zurück. Mit std::filesystem lässt sich

ein Verzeichnishierarchie erzeugen (2). Der ungewöhnlich wirkende Operator /= (3) ist für Pfade überladen. Der Ausdruck fs::create_symlink (4) erzeugt direkt einen symbolischen Link. Die Eigenschaften einer Datei lassen sich auch abfragen (5). Der Aufruf

recursive_directory_iterator (7) ist sehr mächtig. Damit können Verzeichnisse rekursiv traversiert werden. Klar, auf dem Online-Compiler kann kein Verzeichnis gelöscht werden (8).

std::any, std::optional und std::variant

Was haben die neuen Datentypen std:any, std::optional und std::variant gemein? Sie haben alle ihren Ursprung in boost.

std::any ist die richtige Wahl für einen typ-sicheren Container, der einen beliebigen Wert besitzen kann. Beliebiger Wert stimmt nicht ganz genau. std::any fordert, dass seine Werte kopierbar sein müssen.

std::optional steht für einen Datentyp, der einen Wert oder keinen Wert enthalten kann. Er ist durch Haskells Maybe Monade inspiriert.

std::variant ist eine typ-sichere union. Sie kann keine Referenzen oder Arrays annehmen.

Fazit: C++17 – GROSS oder klein?

Ist C++17 nun ein großer C++ Standard in der Tradition von C++98 oder C++11, oder ein kleiner C++ Standard in der Tradition von C++14. Um diese Frage zu beantworten, fehlen noch wichtige Details. Auf diese Frage geht der Autor in seinenBlogbeiträgen zu C++17 näher ein.

Der Autor

Der Autor, Rainer Grimm.
Der Autor, Rainer Grimm.
(Bild: Rainer Grimm)

* Rainer Grimm ist seit vielen Jahren als Softwarearchitekt, Team- und Schulungsleiter tätig. In seiner Freizeit schreibt er gerne Artikel zu den Programmiersprachen C++, Python und Haskell, spricht aber auch gerne auf Fachkonferenzen. Auf seinem Blog Modernes C++ beschäftigt er sich intensiv mit seiner Leidenschaft C++. Seit 2016 steht er auf selbstständigen Beinen. Insbesondere das Vermitteln von Wissen zu modernem C++ ist ihm eine Herzensangelegenheit.

Seine Bücher "C++11 für Programmierer", "C++" und "C++-Standardbibliothek" für die kurz und gut Reihe sind beim Verlag O'Reilly erschienen. Seine englischsprachigen Werke "The C++ Standard Library" und "Concurrency with Modern C++" hat er direkt bei Leanpub veröffentlicht.

Dieser Beitrag stammt ursprünglich von unserem Schwesterportal Embedded-Software-Engineering.de.

(ID:45095872)