Aus Linux-Magazin 10/2016

Modernes C++ in der Praxis – Folge 30

Die wichtigsten Neuerungen im neuen C++17-Standard finden in den Bibliotheken statt: der leichtgewichtige String-Wrapper “string_view”, die parallelisierten Algorithmen der STL, die Dateisystem-Bibliothek oder die praktischen Datentypen “std::optional” und “std::any”.

Bevor dieser Artikel die Neuerungen der C++17-Bibliotheken würdigt, kommt er auf eine offene Hausaufgabe zu sprechen. Der vorige Artikel [1] schloss nämlich mit der Frage, was das Programm

int main()??<
  ??(??)??<??>();
??>

macht. Es setzt Trigraphen [2] ein. Das sind Kombinationen aus drei Buchstaben, die für ein Zeichen stehen. Trigraphen waren notwendig, wenn sich das gewünschte Zeichen nicht per Tastatur eintippen lässt. C++17 bricht mit diesem Erbe von C und erklärt die Zeichenkombinationen »??<« für »{« , »??>« für »}« , »??(« für »[« und »??)« für »]« als nicht mehr zulässig. Schon ein wenig Suchen und Ersetzen löst das Rätsel:

int main(){
  []{}();
}

Löst es fast – denn wofür steht der dadaistische Ausdruck »[]{}()« ? Er ist eine Lambda-Funktion, die »[]« einleitet, die einen leeren Funktionskörper »{}« besitzt und die »()« an Ort und Stelle aufruft. Das ist die Art Humor, über den C++-Entwickler lachen. Kommen wir aber nun zu etwas völlig anderem.

Die “std::string_view”-View

Eine »std:.string_view« [3] ist eine Referenz auf einen String, die den String nicht besitzt. Das klingt komplizierter, als es ist. »std::string_view« repräsentiert eine View (Referenz) auf eine Sequenz von Buchstaben. Das kann ein C++- oder C-String sein. C++17 bietet wie immer vier Typ-Synonyme für jeden zugrunde liegenden Zeichentyp an (Tabelle 1).

Tabelle 1

Typ-Synonyme

Type

Definition

std::string_view

std::basic_string_view<char>

std::wstring_view

std::basic_string_view<wchar_t>

std::u16string_view

std::basic_string_view<char16_t>

std::u32string_view

std::basic_string_view<char32_t>

Warum ist der Bedarf so groß nach der View, dass Google, LLVM und Bloomberg mit »std::string_view« schon selbst etwas Ähnliches implementiert hatten? Es lässt sich billig kopieren, da die View typischerweise nur zwei Informationen enthält: den Zeiger auf die Zeichensequenz und deren Länge.

Um den Umstieg von »std::string« zu »std::string_view« zu erleichtern – das gilt natürlich für die weiteren Typ-Synonyme gleichermaßen –, bietet »std::string_view« ein ähnliches Interface wie »std::string« an, das aus kaum mehr besteht als den lesenden Methoden sowie den neuen Methoden »remove_prefix()« und »remove_suffix()« (Listing 1).

Listing 1

string_view.cpp

01 #include <iostream>
02 #include <string>
03 #include <experimental/string_view>
04
05 int main(){
06
07     std::string str = "   A lot of space";
08     std::experimental::string_view strView = str;
09     strView.remove_prefix(std::min(strView.find_first_not_of(" "), strView.size()));
10     std::cout << "str      :  " << str << std::endl
11               << "strView  : " << strView << std::endl;
12
13     std::cout << std::endl;
14
15     char arr[] = {'A',' ','l','o','t',' ','o','f',' ','s','p','a','c','e','\0', '\0', '\0'};
16     std::experimental::string_view strView2(arr, sizeof arr);
17     auto trimPos = strView2.find('\0');
18     if(trimPos != strView2.npos) strView2.remove_suffix(strView2.size() - trimPos);
19     std::cout << "arr     : " << arr << ", size=" << sizeof arr << std::endl
20               << "strView2: " << strView2 << ", size=" << strView2.size() << std::endl;
21
22 }

Das Programm birgt keine großen Überraschungen. In den Zeilen 8 und 16 erhalten die »std::string_view« -s Referenzen auf den C++-String beziehungsweise das Zeichenarray. Während »strView.find_first_not_of(” “)« in Zeile 9 alle führenden Zeichen entfernt, die kein Leerzeichen sind, löscht »strView2.find(‘\0’)« in Zeile 18 alle abschließenden Spaces.

Um das Programm auszuführen, bietet sich der Online-Compiler [4] an. Mit Hilfe eines hochaktuellen GCC 6.1 mit dem Flag »-std=C++17« lässt sich die neue Funktionalität in Aktion bewundern (Abbildung 1). Noch muss der Programmierer in Listing 1 den Namensraum »experimental« verwenden. Das wird unnötig, wenn C++17 verabschiedet ist.

Abbildung 1: Das Programm aus <a href="#article_l1" class="listing" title=

Listing 1 in Aktion.” width=”300″ height=”151″ /> Abbildung 1: Das Programm aus Listing 1 in Aktion.

Parallele Algorithmen

Weiter geht es mit alten Bekannten: der Standard Template Library. Sie enthält gut 100 Algorithmen für das Suchen, Zählen, Manipulieren von Bereichen und deren Elementen. C++17 überlädt die meisten der Algorithmen, Entwickler können sie mit einer so genannte Execution Policy aufrufen. Die bestimmt, ob das Programm den Algorithmus sequenziell, parallel oder vektorisiert ausführt.

Vektorisierung benutzt die SIMD-Erweiterung (Single Instruction, Multiple Data, [5]) moderner CPUs, die eine Operation parallel auf mehreren Daten ausführen kann. Welche überladene Version eines Algorithmus zum Tragen kommt, steuert der Anwender über das Policy Tag (Listing 2). Das Dokument P00024R2 [6] erklärt das Ganze.

Listing 2

Parallele Algorithmen

01 using namespace std;
02 vector<int> v = ...
03
04 // standard sequential sort
05 sort(v.begin(), v.end());
06
07 // explicitly sequential sort
08 sort(sequential, v.begin(), v.end());
09
10 // permitting parallel execution
11 sort(par, v.begin(), v.end());
12
13 // permitting vectorization as well
14 sort(par_vec, v.begin(), v.end());

Schön ist an dem kleinen Beispiel in Zeile 5 zu sehen, dass die klassische Variante von »std::sort« mit C++17 immer noch zu Verfügung steht. Dagegen lässt sich mit C++17 jetzt explizit die sequenzielle (Zeile 8), die parallele (Zeile 11) oder auch die vektorisierende (Zeile 14) Variante von »std::sort« anfordern. Doch Vorsicht: Für die richtige Anwendung der Algorithmen ist der Anwender verantwortlich. Die Algorithmen schützen nicht per se vor Race-Conditions (kritischer Wettlauf) oder Deadlocks (Verklemmungen). Welche parallelen Algorithmen C++17 anbietet, kann das erwähnte Dokument P00024R2 [6] in Abbildung 2 ebenfalls erklären.

Abbildung 2: Liste der parallelen Algorithmen in C++17.

Abbildung 2: Liste der parallelen Algorithmen in C++17.

Die neue Dateisystem-Lib

Eine Bibliothek zum Zugriff auf Dateisysteme hatten viele C++-Entwickler lange vermisst. Sie basiert auf »boost::filesystem« [7] und besitzt die Besonderheit, dass ihre Komponenten optional sind. Das heißt, die Abstraktionen, die »std::filesystem« auf die konkreten Dateisysteme abbildet, brauchen nicht implementiert zu sein. Das ergibt natürlich einen Sinn, denn zum Beispiel kennt FAT-32 keine Symlinks.

Die Bibliothek basiert auf den drei Konzepten »file« (Datei),»file name« (Dateiname) und »path« (Pfad). Eine Datei kann ein Verzeichnis, ein harter Link, symbolischer Link oder eine reguläre Datei sein. Bei Pfaden zu Dateien unterscheidet die Bibliothek zwischen absoluten und relativen. Für das Lesen, Manipulieren oder Erzeugen der drei Komponenten bietet C++17 ein reichhaltiges Interface an. Listing 3 gibt einen ersten Einblick (Details siehe [8]).

Listing 3

filesystem.cpp

01 #include <fstream>
02 #include <iostream>
03 #include <string>
04 #include <experimental/filesystem>
05 namespace fs = std::experimental::filesystem;
06
07 int main(){
08
09     std::cout << "Current path: " << fs::current_path() << std::endl;
10
11     std::string dir= "sandbox/a/b";
12     fs::create_directories(dir);
13
14     std::ofstream("sandbox/file1.txt");
15     fs::path symPath= fs::current_path() /=  "sandbox";
16     symPath /= "syma";
17     fs::create_symlink("a", "symPath");
18
19     std::cout << "fs::is_directory(dir): " << fs::is_directory(dir) << std::endl;
20     std::cout << "fs::exists(symPath): "  << fs::exists(symPath) << std::endl;
21     std::cout << "fs::symlink(symPath): " << fs::is_symlink(symPath) << std::endl;
22
23
24     for(auto& p: fs::recursive_directory_iterator("sandbox"))
25         std::cout << p << std::endl;
26     // fs::remove_all("sandbox");
27
28 }

Die Methode »fs::current_path()« aus Zeile 9 gibt den aktuellen Pfad zurück. Sogar mehrere Verzeichnishierarchien zu erzeugen gelingt »std::filesystem« (Zeile 12). Für den »path« -Datentyp überlädt Zeile 16 den »/=« -Operator, sodass Zeile 17 direkt einen Symlink erzeugt. Die Zeilen 19 bis 21 fragen den »file« -Objekttyp ab. Interessant ist »recursive_directory_iterator()« in Zeile 24, der ganze Verzeichnisstrukturen traversieren kann. Das Löschen der neuen Verzeichnisbäume ist per Online-Compiler [4] nicht möglich, daher ist der Befehl nur angedeutet. Abbildung 3 zeigt die Ausgabe des Programms.

Abbildung 3: Dateisystem-Manipulationen auf Cppreference.com.

Abbildung 3: Dateisystem-Manipulationen auf Cppreference.com.

Der neue Beliebigkeitstyp

Wie »std::filesystem« fußen »std::any« und »std::optional« auf den Boost-Bibliotheken [9]. »std::any« geht Programmierern praktisch zur Hand, wenn sie Container mit beliebigen Typen erzeugen möchten. Beliebig trifft nicht ganz zu, denn »std::any« fordert, dass sich Objekte eines Typs kopieren lassen (Copy-konstruierbar sein müssen). Der kleine Rest der Theorie zu »std::any« ist schnell an dem Beispiel in Listing 4 erklärt.

Listing 4

any.cpp

01 #include <iostream>
02 #include <string>
03 #include <vector>
04 #include <any>
05
06 struct MyClass{};
07
08 int main(){
09
10     std::cout << std::boolalpha;
11
12     std::vector<std::any> anyVec(true,2017,std::string("test"),3.14,MyClass());
13     std::cout << "std::any_cast<bool>anyVec[0]: " << std::any_cast<bool>(anyVec[0]); // true
14     int myInt= std::any_cast<int>(anyVec[1]);
15     std::cout << "myInt: " << myInt << std::endl;         // 2017
16
17     std::cout << std::endl;
18     std::cout << "anyVec[0].type().name(): " << anyVec[0].type().name();             // b
19     std::cout << "anyVec[1].type().name(): " << anyVec[1].type().name();             // i
20
21 }

Die Ausgabe des Programms wartet bereits im Sourcecode. Zeile 12 erklärt einen »std::vector<std::any>« , der beliebige Copy-konstruierbare Typen annehmen darf. Um auf die einzelnen Elemente zuzugreifen, muss der Entwickler einen »std::any_cast« anwenden. Passt der Typ nicht, antwortet die C++-Laufzeitumgebung mit einer »std::bad_any_cast« -Ausnahme. Auf den Elementen des Vektors lässt sich direkt der Typ abfragen. Über die Details zu »std::any« [10] informiert wieder Cppreference.com zuverlässig.

Der neue Unklar-Datentyp

»std::optional« [11], eigentlich schon für den kleinen Standard C++14 vorgesehen, steht für eine Berechnung, die einen Wert ergibt – oder auch nicht. So müssen der Find-Algorithmus oder die Abfrage einer Hashtabelle damit umgehen, wenn die Anfrage nicht beantwortet wird. Etabliert sind spezielle Werte wie Nullzeiger, leere Strings oder besondere Integerwerte, die für das Fehlen eines Resultats stehen, so genannte Nicht-Ergebnisse. Diese Technik ist aufwändig und fehleranfällig, da der Programmierer Nicht-Ergebnisse stets besonders behandeln muss und sie sich syntaktisch nicht von regulären Ergebnissen abheben.

Hier hilft »std::optional« , das bei einem Nicht-Ergebnis keinen Wert enthält. Listing 5 demonstriert die Technik genauer: Die Funktion »getFirst()« in Zeile 5 verwendet »std::optional« . Sie gibt das erste Element zurück, falls es existiert (Zeile 6). Andernfalls kommt ein »std::optional<int>« -Objekt zurück (Zeile 7).

Listing 5

optional.cpp

01 #include <iostream>
02 #include <vector>
03 #include <experimental/optional>
04
05 std::experimental::optional<int> getFirst(const std::vector<int>& vec){
06   if ( !vec.empty() ) return std::experimental::optional<int>(vec[0]);
07   else return std::experimental::optional<int>();
08 }
09
10 int main(){
11
12     std::vector<int> myVec{1,2,3};
13     std::vector<int> myEmptyVec;
14
15     auto myInt= getFirst(myVec);
16
17     if (myInt){
18         std::cout << "*myInt: "  << *myInt << std::endl;
19         std::cout << "myInt.value(): " << myInt.value() << std::endl;
20         std::cout << "myInt.value_or(2017):" << myInt.value_or(2017) << std::endl;
21     }
22
23     std::cout << std::endl;
24
25     std::experimental::optional<int> myEmptyInt= getFirst(myEmptyVec);
26
27     if (!myEmptyInt){
28         std::cout << "myEmptyInt.value_or(2017):" << myEmptyInt.value_or(2017) << std::endl;
29     }
30
31 }

»main()« benutzt zwei Vektoren. Die Aufrufe »getFirst()« (Zeilen 15 und 25) geben die »std::optional« -Objekte zurück. Bei »myInt« in Zeile 17 enthält das Objekt einen Wert, bei »myEmptyInt« in Zeile 27 keinen. Nun lässt sich der Wert von »myInt« ausgeben (Zeilen 18 bis 20). Die Methode »value_or()« in den Zeilen 20 und 28 liefert, wenn das »std::optional« -Objekt überhaupt einen Wert enthält, diesen oder einen Defaultwert.

Wie geht’s weiter? Anders!

Als Autor muss man den Überblick haben: 30 Folgen und damit fünf Jahre alt ist meine Serie zu modernem C++ mittlerweile. Ging es in der Anfangszeit vor allem um C++11, so kamen aus aktuellen Anlässen Artikel zu C++14- und C++17-Features hinzu. Die frohe Botschaft: Die technischen Grundlagen zu allen modernen Spielarten des Programmiersprachen-Klassikers C++ hat die Serie nun ausreichend erklärt.

Abbildung 4: »std::optional« fragt einen Vektor ab.

Abbildung 4: »std::optional« fragt einen Vektor ab.

Darum werde ich den Fokus meiner Artikel anpassen. Künftig wird es darum gehen, das technische Wissen zu modernem C++ sinnvoll und richtig einzusetzen. Denn die Praxis ist, laut W. I. Lenin, das Kriterium für die Wahrheit (jk)

Der Autor

Rainer Grimm ist selbstständiger Trainer und Coach für modernes C++ und Python. Seine Bücher “C++11 für Programmierer”, “C++” und “C++11-Standardbibliothek” sind bei O’Reilly erschienen.

DIESEN ARTIKEL ALS PDF KAUFEN
EXPRESS-KAUF ALS PDFUmfang: 5 HeftseitenPreis €0,99
(inkl. 19% MwSt.)
LINUX-MAGAZIN KAUFEN
EINZELNE AUSGABE Print-Ausgaben Digitale Ausgaben
ABONNEMENTS Print-Abos Digitales Abo
TABLET & SMARTPHONE APPS Readly Logo
E-Mail Benachrichtigung
Benachrichtige mich zu:
0 Kommentare
Älteste
Neuste Beste Bewertung
Inline Feedbacks
Alle Kommentare anzeigen
Nach oben