Kapitel 5. Die Standardbibliothek

In diesem Kapitel:

Die meisten Erweiterungen der Standardbibliothek haben sich schon lange im Einsatz bewährt, sind sie doch aus dem Boost-Projekt (boost, 2011) hervorgegangen und dem Technical Report 1 (C++ Technical Report 1, 2011) 2005 als Ergänzung zum aktuellen C++-Standard hinzugefügt worden. Aber auch neue Komponenten kamen hinzu, und die Funktionalität der C++98-Bibliothek wurde an die mächtigere Kernfunktionalität von C++11 angepasst (Abbildung 5.1).

Einflüsse auf die neue C++11- Standardbibliothek

Die großen Highlights im Überblick:

TR1:

Neue Komponenten in C++11:

Nach diesem kurzen historischen Abriss über die C++11-Standardbibliothek folgt die neue Funktionalität in kompakter Form. Zuerst stelle ich die neuen Bibliotheken dar und anschließend die Bibliotheken, die bestehende Konzepte von C++98 aufgreifen, erweitern und abrunden.

Die Motivation für eine Bibliothek für reguläre Ausdrücke ähnelt in gewisser Weise der der Multithreading-Bibliothek. Viele Plattformen haben proprietäre Erweiterungen, um mit regulären Ausdrücken zu arbeiten. In C++11 gibt es eine standardisierte Bibliothek. Die neue regex-Bibliothek bietet eine einheitliche Schnittstelle an, um reguläre Ausdrücke anzuwenden, sodass die resultierenden C++11-Programme per se portabel sind.

Drei Schritte

Der Umgang mit regulären Ausdrücken in C++11 erfolgt typischerweise in drei Schritten.

In Listing 5.1 sind diese drei Schritte exemplarisch dargestellt. rgx ist der reguläre Ausdruck, der mit dem Raw-String initialisiert wird. Dabei repräsentiert \d+ eine Zahl, die aus mindestens einer Ziffer besteht. smatch soll das Ergebnis der Suche halten. Dieses Ergebnis wird durch die letzte Zeile angefordert. smatch.prefix() gibt als Ergebnis das Präfix »abc« zurück.

Neben regex_search sind regex_match und regex_replace klassische Anwendungsfälle für reguläre Ausdrücke:

In Listing 5.2 sind der regex_seach-Codeschnipsel aus Listing 5.1, aber auch regex_match und regex_replace im Einsatz.

regexNumber.cpp

std::regex_match (Zeile 18) kommt in diesem Anwendungsfall genauso wie std::regex_replace (Zeile 30) ohne ein Objekt aus, das das Ergebnis der Suche hält. Gerade der regex_replace-Ausdruck ist relativ anspruchsvoll zu lesen. Paraphrasiert lautet er: Ersetze im String von text.begin() bis text.end() alle Vorkommen des regulären Ausdrucks rgx mit replString, indem du das Ergebnis an den String result hinten anhängst: back_inserter(result). In diesem konkreten Fall werden auch die nicht modifizierten Teilstrings von text mit hinten angehängt. Genau das zeigt die Ausgabe des Programms in Abbildung 5.3.

Mächtigkeit von regulären Ausdrücken

Die regulären Ausdrücke in C++11 können noch viel mehr. Dies sind ein paar Punkte, die in Kapitel 19 im Abschnitt „Reguläre Ausdrücke“ unser Thema sein werden:

  • Regular-Expression-Syntax

  • Umgang mit anderen Zeichentypen als char

  • Arbeiten mit Erfassungsgruppen

  • Charakter- und Token-Ströme über Match-Objekten

Ein kleines Beispiel soll aber noch zum Abschluss folgen. Iteriere über die Zahlen (Tokens) eines Strings.

regexTokenStream.cpp

Der Sourcecode ist ungewöhnlich kompakt für C++. Das einzig Neue ist der Token-Iterator (Zeile 16), über den, mittels des regulären Ausdrucks parametrisiert, in der while-Schleife (Zeile 20) iteriert wird. Der Programmlauf gibt die natürlichen Zahlen aus.

Ist die Bibliothek für reguläre Ausdrücke sowohl für den Einsteiger als auch für den Profi von großem Nutzen, so war Template-Metaprogrammierung bisher dem C++-Profi vorbehalten. Das ändert sich aber mit der neuen Type-Traits-Bibliothek.

Bei der Template-Metaprogrammierung instanziiert der Compiler die Templates und erzeugt durch diesen Prozess den temporären C++-Sourcecode, der zusammen mit dem restlichen Sourcecode übersetzt wird.

Klassiker der Template-Metaprogrammierung

Ein Klassiker in der C++-Template-Metaprogrammierung sind Klassen-Templates, die zur Übersetzungszeit Charakteristiken eines Typs evaluieren. Das Programm typeTraits.cpp in Listing 5.4 evaluiert zur Übersetzungszeit, ob der abgefragte Typ eine Klasse darstellt, sodass das Ergebnis zur Laufzeit zur Verfügung steht.

typeTraits.cpp

Das Programm in Listing 5.4 ermittelt für die Datentypen std::string und int, dass std::string eine Klasse darstellt, der Built-in-Datentyp int aber nicht. Sowohl das Klassen-Template IsClass (Zeilen 25 bis 28) als auch die neue C++11-Funktionalität std::is_class (Zeilen 33 bis 36) bringen das erwartete Ergebnis.

Magie der Template-Metaprogrammierung

Relativ schwierig zu verstehen ist das Klassen-Template IsClass, das über einen Typ parametrisiert wird. Die Magie der Template-Instanziierung ist aber schnell aufgedeckt. Dazu betrachten wir IsClass<T>::Yes für diese zwei Fälle:

Neben Typabfragen sind mit der Type-Traits-Bibliothek Vergleiche und sogar Transformationen von Datentypen möglich. Dies sind für den fortgeschrittenen C++-Programmierer die Werkzeuge, um Algorithmen zu schreiben, die auf seinen Datentyp optimal angepasst sind.

Neu ist auch die Zufallszahlenbibliothek in C++11.

Zufallszahlen werden in vielen Bereichen in der Softwareentwicklung benötigt, sei es für das Testen von Software oder das Erzeugen von kryptografischen Schlüsseln.

Zufallszahlengenerator

Der Zufallszahlengenerator in C+11 besteht aus zwei Teilen: einem Generator, der einen Strom von Zufallszahlen erzeugt, und einer Verteilung, die die Werte in einem vorgegebenen Bereich verteilt. Die Verteilung wird über den Generator parametrisiert. Damit der Generator nicht jedes Mal mit der gleichen Zufallszahl startet und somit die gleiche Folge von Zufallszahlen erzeugt, wird der sogenannte seed benötigt.

Das Spiel kann beginnen:

Das Programm dazu in Listing 5.5 ist kurz und bündig:

randomNumbers.cpp

seed

Mit dem seed-Aufruf (Zeile 11) wird in Listing 5.5 der Generator initialisiert. Dieser Generator wird an die Verteilung übergeben (Zeile 14), sodass der resultierende Zufallsgenerator (Zeile 17) auf Anfrage die Zufallszahlen produziert.

Tiefere Einsichten in die Erzeugung von Zufallszahlen gibt es in Kapitel 19 im Abschnitt „Zufallszahlen“. Dies umfasst vor allem die vielen verschiedenen Generatoren und Verteilungen, die C++11 von Hause aus mitbringt.

Ähnlich nützlich wie die Zufallszahlenbibliothek ist die neue Zeitbibliothek.

Ein Referenz-Wrapper ist ein kopierkonstruierbarer und zuweisbarer Wrapper um ein Objekt vom Typ T&. Damit lösen Sie das bekannte Problem in C++, dass Referenzen nicht die notwendigen Eigenschaften für Standardcontainer mitbringen. Ein Ausdruck der Form std::vector<int&> quittiert der GCC-Compiler mit einer langen Fehlermeldung. Durch die Verwendung von std::reference_wrapper<int> in Listing 5.7 ist er aber möglich.

referenceWrapper.cpp

Mit der Initialisiererliste (Listing 5.7, Zeile 16) wird der Vektor mit dem Referenz-Wrapper über die drei natürlichen Zahlen initialisiert. std::ref und std::cref sind zwei Hilfsfunktionen, die einfach eine Referenz oder einen konstanten Referenz-Wrapper erzeugen. Der entscheidende Punkt befindet sich in Zeile 23, denn darin wird die Referenz von b auch im Container myIntRefVector modifiziert, sodass dessen Wert auf 2011 verändert wird.

Damit verlassen wir das Gebiet der neuen Bibliotheken in C++11 und widmen uns den überarbeiteten C++11-Bibliotheken. Diese bieten bewährte C++-Funktionalität in generischer Form an. Sowohl der Novize als auch der Profi profitieren von diesen Erweiterungen – der Novize, da sich die Bibliotheken einfacher ansprechen lassen und daher deren Benutzung weniger fehleranfällig ist, der Profi, da das Laufzeitverhalten seines Programms insbesondere von den neuen Containern profitiert.

Viele bewährte C++-Bibliotheken wurden in C++11 runderneuert. Dies betrifft die Smart Pointer, die neben dem C++-Smart-Pointer std::auto_ptr die neuen Smart Pointer std::shared_ptr, std::weak_ptr und std::unique_ptr enthalten. Dies betrifft die neuen Container, indem dem std::pair ein std::tuple, dem std::vector ein std::array und dem std::map ein std::unordered_map in C++11 gegenübergestellt wurden. Dies betrifft den C++11-Funktionsadapter std::bind, der die klassischen Funktionsadapter std::bind1st und std::bind2nd in C++ deutlich erweitert und dessen resultierende Objekte an std::function gebunden werden können.

shared_ptr, weak_ptr und unique_ptr

Die neuen Smart Pointer std::shared_ptr und std::weak_ptr, die schon lange in der Boost-Bibliothek (boost, 2011) im Einsatz sind, und der neue Smart Pointer std::unique_ptr gelten als eine, wenn nicht gar die wichtigste Erweiterung im neuen C++11-Standard. Diese drei erweitern deutlich die Funktionalität des klassischen std::auto_ptr und räumen mit seinen konzeptionellen Schwächen auf. Das ist der Grund dafür, dass dieser in C++11 als deprecated erklärt wird und stattdessen ein std::unique_ptr verwendet werden sollte. In der Tabelle 5.2 sind die wichtigsten Charakteristiken der Smart Pointer von C++11 zusammengefasst.

Aus welchem Grund wurde der std::auto_ptr als deprecated erklärt?

auto_ptr

Der std::auto_ptr besitzt zwei Eigenschaften, die leicht zu undefiniertem Verhalten des Programms führen:

Während der Compiler in der Regel bemerkt, wann ein std::auto_ptr in einem STL-Container verwendet wird, ist das implizite Verschieben der Ressource beim Kopieren eine häufige Fehlerquelle.

autoPtrCopy.cpp

Listing 5.8 bringt es auf den Punkt. In dem Ausdruck auto2(auto1) wird der Inhalt von auto1 nach auto2 verschoben (Abbildung 5.8).

Interessant sind sowohl das Kompilieren als auch das Ausführen des Programms.

Der Compiler moniert die Verwendung von std::auto_ptr mit einer deprecated-Warnung.

Das Ausführen des Programms führt zu einem Laufzeitfehler.

unique_ptr

Als der std::auto_ptr deprecated erklärt wurde, musste ein Ersatz geschaffen werden: der neue C++11-Smart-Pointer std:unique_ptr. Dieser ist nahezu aufrufkompatibel zum std::auto_ptr und besitzt auch seine Ressource exklusiv. Wenn der std::unique_ptr seine Gültigkeit verliert (out of scope), wird sein Destruktor aufgerufen und gleichzeitig die Ressource des std::unique_ptr zerstört. Im Gegensatz zum std::auto_ptr unterstützt der std::unique_ptr kein Kopieren, sondern nur das explizite Verschieben seiner Ressource durch die neue Funktion std::move. Das explizite Verschieben einer Ressource mit std::unique_ptr analog zum impliziten Kopieren einer Ressource mit std::auto_ptr (Listing 5.8) ist schnell implementiert.

uniquePtrMove.cpp

In Abbildung 5.11 ist das explizite Transferieren der Ressource grafisch dargestellt.

shared_ptr

Während der std::unique_ptr eine 1:1-Beziehung zu seiner Ressource besitzt, ist der typische Einsatzbereich des std:shared_ptr, eine gemeinsame Ressource zu nutzen. Jeder std::shared_ptr besitzt einen Zeiger auf seine Ressource und den Referenzzähler. Wird nun ein std::shared_ptr kopiert, referenziert dieser sowohl die gemeinsame Ressource als auch den Referenzzähler. Beim Erzeugen eines std::shared_ptr wird dessen Referenzzähler auf 1 gesetzt. Beim Kopieren wird er um 1 inkrementiert, beim Löschen um 1 dekrementiert. Erreicht der Referenzzähler den Wert 0, führt dies zum automatischen Löschen der Ressource. Damit ist er kopierkonstruierbar und zuweisbar und kann in den Containern als STL-Bibliothek verwendet werden.

Abbildung 5.12 zeigt exemplarisch das Kopieren eines std::shared_ptr.

weak_ptr

Abgerundet wird die Funktionalität des std::shared_ptr durch den std::weak_ptr, denn dieser hilft, zyklische Referenzen von std::shared_ptr aufzubrechen. Der std::weak_ptr verändert nicht den Zähler auf die gemeinsam genutzte Ressource. Genau genommen ist der std::weak_ptr kein Smart Pointer, denn er bietet keinen transparenten Zugriff auf die Ressource an. Er verfügt nur über ein einfaches Interface auf eine Ressource, die von einem std::shared_ptr verwaltet wird. Um die Ressource eines std::weak_ptr zu adressieren, muss dieser zuerst gelockt werden, sodass anschließend über einen initialisierten std:shared_ptr auf dessen Ressource zugegriffen werden kann.

Listing 5.10 zeigt den einfachen Umgang mit std:shared_ptr und std:weak_ptr.

sharedWeakPtr.cpp

Die Methode use_count des std:shared_ptr in Listing 5.10 gibt den Wert des Referenzzählers aus. In Zeile 8 wird der sharedPtr erzeugt. Der Referenzzähler besitzt den Wert 1. localSharedPtr (Zeile 14) erhöht den Referenzzähler um 1. Am Ende des lokalen Blocks verliert dieser seinen Gültigkeitsbereich, sodass er um 1 dekrementiert wird (Zeile 19). Der weakPtr, der über den sharedPtr initialisiert wird, erhöht nicht den Referenzzähler (Zeile 22). Wird die Ressource des weakPtr verwendet, um damit den localSharedPtr zu initialisieren, wird dessen Ressourcenzähler inkrementiert (Zeile 27).

Genau dieses Verhalten zeigt das Programm:

Neben dem Smart Pointer erweitern die neuen Container in C++11 die bestehenden C++-Container deutlich. Dies betrifft bei std::tuple dessen Mächtigkeit, da es im Gegensatz zu std::pair beliebig viele Argumente annehmen kann, dies betrifft bei std::array und den Hashtabellen deren Performance gegenüber den klassischen sequenziellen Containern oder assoziativen Arrays.

tuple

std::tuple ist ein heterogener Container fester Länge. Er kann beliebig viele Argumente annehmen. Dies ist möglich, da std::tuple ein Variadic Template ist (siehe Kapitel 9, Abschnitt „Variadic Templates“). Ein Tupel lässt sich über einen Konstruktoraufruf oder die Hilfsfunktion std::make_tuple einfach erzeugen. Die instanziierten Tupel können verglichen, gelesen und modifiziert werden (Listing 5.11).

tuple.cpp

Der umständliche Zugriff auf die Elemente des Tupels std::get<0> (tup1) (Zeile 14) ist der Tatsache geschuldet, dass get ein Template und der Index eine Compile-Zeitkonstante ist.

Abbildung 5.14 zeigt die Ausgabe des Programms.

Dank auto geht das Definieren eines Tupels deutlich einfacher von der Hand.

auto tup= std::make_tuple("second",4,1.1,true,'a');

array

Der neue sequenzielle Container Array hat mit dem Tupel gemein, dass er eine feste Länge besitzt. std::array bietet das Laufzeitverhalten des C-Arrays mit der Schnittstelle des C++-Vektors an. Damit ist er STL-konform und kann deren Algorithmen verwenden (Listing 5.12).

array.cpp

Zu der einfachen Arithmetik in Listing 5.12 noch ein paar Bemerkungen. Die Lambda-Funktion [&sum](int v) { sum += v; } (Zeile 19) bindet sich per Referenz an die globale Variable sum, die die Zahlen aufsummiert. Durch die Lambda-Funktion [](int& v) { v=v*v; } (Zeile 22) lassen sich die Elemente des Arrays direkt quadrieren, da die Argumente per Referenz adressiert werden. Nun fehlt nur noch die Ausgabe.

C-Array, C++-Vektor und C++11-Array

Tabelle 5.3 stellte die Charakteristiken der drei sequenziellen Datentypen C-Array, C++-Vektor und C++11-Array gegenüber.

Der neue Container std::forward_list ist eine einfach verkettete Liste und kann nur vorwärts durchlaufen werden.

std:forward_list ist optimiert für schnelles Einfügen und Entfernen von Elementen, bietet aber keinen wahlfreien Zugriff auf seine Elemente an. Bedingt durch seine Struktur, besitzt sie ein eingeschränktes und eigenwilliges Interface und bricht mit bekannten Konventionen aus der Standard Template Library. So sucht man bei ihr beispielsweise vergeblich eine size- oder push_back-Methode.

Den einfachen Umgang mit der std::forward_list zeigt Listing 5.13.

forwardList.cpp

Das umständliche Initialisieren der std::forward_list in Listing 5.13 (Zeilen 10 bis 16) ist der Tatsache geschuldet, dass das Programm mit dem VC10-Compiler von Microsoft übersetzt wurde. VC10 unterstützt keine Initialisiererlisten (Zeile 8) und auch keine Range-basierte For-Schleife in den Zeilen 19, 29 und 36. Da die std::forward_list keinen wahlfreien Zugriff erlaubt, setzt das Entfernen eines Elements einen Iterator auf einem Element (Zeilen 27 und 34) voraus. myForList.before_begin() gibt einen Iterator vor dem ersten Element zurück. Abbildung 5.16 zeigt die Ausführung des Programms.

Eines der im C++98-Standard am häufigsten vermissten Features sind Hashtabellen, auch unter dem Namen Dictionary oder assoziatives Array bekannt.

C++ versus C++11 assoziative Arrays

Es wog aber nicht so schwer, dass es keine Hashtabellen in C++98 gab. Mit std::map und std::set bzw. std::multimap und std::multiset gibt es Datenstrukturen in C++98, die sich nahezu wie Hashtabellen verhalten, denn sie erlauben den schlüsselbasierten Zugriff auf ihre Elemente. Doch in zwei Punkten unterscheiden sie sich davon. Die klassischen Maps und Sets

Aus diesem Grund stellten viele Compiler-Hersteller eigene Bibliotheken zur Verfügung. Damit waren die intuitiven Namen für die neuen C++11-Hashtabellen vergeben, und die C++11-Hashtabellen erhielten recht sperrige Namen:

Zwei Tabellen helfen, den Überblick über die acht Container zu behalten, die doch sehr ähnlich sind. Zuerst eine Gegenüberstellung der Container, die ein ähnliches Interface anbieten:

Die zweite Charakteristik der assoziativen Container lässt sich am besten als Frage formulieren:

Exemplarisch wird dies anhand der neuen C++11-Container in Tabelle 5.5 dargestellt.

Der Einsatz der neuen assoziativen Container ist immer dann überlegenswert, wenn die Datenstruktur relativ groß ist und keine Ordnung auf den Schlüsseln benötigt wird. Der Umstieg von den alten auf die neuen assoziativen Container wird dadurch erleichtert, dass beide ein sehr ähnliches Interface anbieten (Listing 5.14).

unorderedMap.cpp

Listing 5.14 zeigt, dass das Initialisieren, das Schreiben und das Lesen der Elemente von std::map und std::unordered_map der gleichen Syntax folgen. Lediglich bei der Ausgabe variiert es, denn die Schlüssel/Wert-Paare sind bei std::map nach den Schlüsseln aufsteigend sortiert.

Zu den vielen bekannten bringt C++11 noch knapp 20 neue Algorithmen mit. Diese Algorithmen helfen, einfache logische Zusicherungen auf Bereichen zu verifizieren, Bereiche zu kopieren oder schnell neue Werte in einem Bereich zu erzeugen. Aber auch neue Algorithmen rund um Partitionen, rund ums Sortieren und rund um die Datenstruktur Heap stehen zur Verfügung.

std::all_of, std::any_of und std::none_of für die logische Zusicherung auf Bereichen, std::copy_if und std::copy_n als weitere Kopieralgorithmen, std::iota für das schnelle Erzeugen von Werten sind nur ein paar der neuen Algorithmen.