Die C++-Standardbibliothek
Übersicht
Die C++-Standardbibliothek ist Bestandteil der Sprache C++ und im C++-Standard spezifiziert. Sie enthält unter anderem generische Datenstrukturen (sogenannte Container) zum Abspeichern von Daten sowie Algorithmen, um die abgelegten Daten zu bearbeiten. Der Kernpunkte der Bibliothek ist die Trennung zwischen der Datenspeicherung in Containern und deren Bearbeitung mittels der Algorithmen.
Über die C++-Standardbibliothek alleine ließe sich ein umfangreiches Tutorial schreiben, deshalb soll dieses Tutorial nur als Einstieg dienen und beschränkt sich auf die am häufigsten verwendeten Bibliothekskomponenten.
Bibliothekskomponenten
Container
Container dienen zum Abspeichern von Daten eines beliebigen Datentyps. Das Besondere an einem Container ist, dass sich seine Größe automatisch an die Anzahl der abzulegenden Daten anpasst (bis auf eine Ausnahme).
Algorithmen
Algorithmen verarbeiten die in einem Container abgelegten Elemente. So gibt es z.B. Algorithmen, um Daten in einem Container zu suchen oder zu sortieren.
Iteratoren, Ranges und Views
Iteratoren sind Objekte, die auf eine Position innerhalb eines Containers verweisen, d.h., sie entsprechen vom Prinzip her einem Zeiger auf ein Element in einem Feld.
Ein Range ist ein Bereich der einen Anfang und ein Ende hat. Mithilfe eines Ranges lässt sich die Anwendung vieler Algorithmen der Standardbibliothek vereinfachen.
Ein View definiert eine Sicht auf einen Range. Damit ist es z.B. möglich, einen Algorithmus nur auf bestimmte Elemente in einem Range anzuwenden.
Funktionsobjekte und Adapter
Funktionsobjekte sind Objekte, deren Klasse den Operator ()überlädt. Sie sind das C++-Gegenstück zu Funktionszeiger.
Und ein Adapter passt eine vorhandene Schnittstelle an eine gewünschte, abweichende Funktion an.
Sonstige Komponenten
Außer diesen datenzentrierten Komponenten enthält der Standardbibliothek viele weitere Funktionen, wie numerische Funktionen (z.B. sin() oder log10()) oder auch Funktionen zur Verarbeitung eines Kalenderdatums oder einer Uhrzeit (system_clock() oder duration()).
Gemeinsame Container-Eigenschaften und -Operationen
Alle Container verfügen über bestimmte allgemein gültige Eigenschaften und Operatoren.
Container-Eigenschaften
1. Alle Container verwenden die sogenannte Werte-Semantik. D.h., die Container kopieren die abzulegenden Daten. Wird ein Datum danach verändert, hat dies keine Auswirkung auf das Datum im Container.
2. Alle Elemente eines Containers besitzen eine definierte Reihenfolge (Ordnung). So werden die Daten im stack-Container in der Reihenfolge abgelegt, in der sie eingefügt wurden. Andere Container, wie z.B. eine priority_queue, sortiert die Elemente standardmäßig nach ihrem Wert.
3. Operationen auf Container sind nicht 'sicher', d.h., die Anwendung muss sicherstellen, dass die Operation definiert ist. So muss z.B. sichergestellt sein, dass beim Aufruf der Methode zum Auslesen eines Datums aus einer priority_queue diese nicht leer ist.
4. Wird ein Container gelöscht, löscht er alle in ihm enthaltenen Elemente.
Beachten Sie, dass beim Ablegen eines Objekts in einem Container immer eine Kopie dess Objekts abgelegt wird (copy-ctor).
5. Container können wiederum Elemente von anderen Containern sein.
6. Die Eigenschaft value_type eines Containers 'enthält' den Datentyp der Elemente des Containers. Mithilfe dieser Eigenschaft können Variablen/Objekte definiert werden, die den gleichen Datentyp besitzen wie die Container-Elemente.
value_type wird hauptsächlich eingesetzt, wenn Container an Templates übergeben werden, da dort der Containertyp und oft der Datentyp der im Container abgelegten Elemente benötigt wird.
Container-Operationen
Containerdefinition
Jeder Container besitzt einen Standardkonstruktor, der einen leeren Container (ohne Elemente) erstellt. Außer dem Standardkonstruktor besitzen Container den Kopierkonstruktor, um einen Container zu duplizieren. Des Weiteren ist es möglich, einen Container bei seiner Definition mit Elementen aus einem anderen Container zu initialisieren. Der Bereich der zu übernehmenden Elemente wird dabei durch Iteratoren bestimmt und der Ziel-Container muss nicht den gleichen Datentyp wie der Quellen-Container besitzen.
Bei den meisten Containern kann beim Erstellen eines Container-Objekts optional eine Funktion für die Reservierung des vom Container benötigten Speicher angegeben werden. Da dies nur in Ausnahmefällen zur Anwendung kommt, wird im Folgenden darauf nicht weiter eingegangen.
Größenangaben
Die Anzahl der Elemente in einen Container kann mit der Methode size() ermittelt werden. Soll lediglich geprüft werden, ob ein Container Elemente enthält, kann hierfür die Methode empty() verwendet werden. Sie liefert true zurück, wenn der Container leer ist.
Vergleiche
Container können mit den üblichen Vergleichsoperatoren, wie z.B. ==, != oder <, verglichen werden. Die zu vergleichenden Container müssen denselben Typ besitzen. Zwei Container sind gleich, wenn ihre Elemente die gleichen Werte besitzen und die Reihenfolge der Elemente identisch ist.
Der Vergleich erfolgt lexikografisch, d.h., die Container werden Element für Element verglichen, bis eine der folgenden Bedingungen eintritt:
- Zwei Elemente sind ungleich; in diesem Fall ist das Ergebnis des Vergleichs das Resultat aus dem Vergleich der beiden Elemente.
- Einer der Container enthält kein weiteres Element mehr; in diesem Fall ist der Container, der kein Element mehr enthält, kleiner.
- Beide Container enthalten keine weiteren Elemente mehr; in diesem Fall sind die beiden Container identisch
Zuweisungen und Vertauschen
Einem Container kann ein anderer Container des gleichen Typs oder eine Initialisiererliste zugewiesen werden. Im ersten Fall werden die Elemente des Quellen-Containers in den Ziel-Container kopiert, d.h., beide Container enthalten danach den gleichen Inhalt. Und mit der Methode swap() kann der Inhalt zweier Container getauscht werden.
Iteratoren
Der sequenzielle Zugriff auf Elemente in einen Container erfolgt entweder mit einer range-for-Schleife oder über Iteratoren. Dafür enthalten die Container die beiden Methoden begin() und end(), die einen Iterator auf das erste bzw. (letzte+1) Element im Container liefern. Iteratoren werden später noch ausführlich behandelt.
Beenden wir damit die Gemeinsamkeiten aller Containern und sehen uns an, welche Bedingungen Objekte bzw. deren Klassen erfüllen müssen, damit diese in einem Container abgelegt werden können.
Objekte und Container
Container, wie auch die später aufgeführten Iteratoren und Algorithmen, stellen an Objekte bzw. deren Klassen folgende Anforderungen:
1. Die Objekte müssen kopierbar sein und die erstellte Kopie muss den gleichen Inhalt wie das Original besitzen. Dieses Verhalten kann durch die Implementierung eines Kopierkonstruktors bzw. Move-Konstruktors erreicht werden.
2. Die Objekte müssen zuweisbar sein, was durch Implementierung eines Zuweisungsoperators bzw. Move-Operators erreicht werden kann.
3. Die Objekte müssen gelöscht werden können. Ein Container entfernt die in ihm abgelegten Objekte durch den Aufruf des Destruktors.
Zur Erfüllung dieser Anforderungen, generiert der Compiler automatisch die entsprechenden Methoden. Für nicht-triviale Klassen, also solche die z.B. dynamische Daten oder andere Objekte enthalten, sind für diese Anforderungen die entsprechenden Methoden explizit zu definieren.
Zusätzlich stellen einige Container und Algorithmen folgende Anforderungen:
4. Der Standardkonstruktor (parameterloser Konstruktor) muss definiert sein. Dies ist immer dann der Fall, wenn ein Container die Reservierung von Speicher für eine vordefinierte Anzahl von Elementen erlaubt.
5. Der Operator == muss definiert sein. Dieser Operator wird immer benötigt, wenn z.B. ein Container durchsucht werden kann. Dabei ist zu beachten, dass der Operator entweder durch eine const-Methode zu implementieren ist oder durch eine friend-Funktion.
6. Können Container auf größer/kleiner verglichen werden oder sind die Elemente innerhalb eines Containers sortierbar, muss zusätzlich zum Operator == der Operator <, oder besser des spaceship-Operator <=>, definiert sein. Auch hier gilt: Die Operatorfunktion muss eine const-Methode oder eine friend-Funktion sein.
Die Konstruktoren von Objekten, die in Containern abgelegt werden, sollten stets als noexcept Konstruktor definiert werden.
CAny::CAny(...) noexcept
{...}
Dadurch wird bei der Übernahme eines Objekts in den Container in den meisten Fällen der Move-Konstruktor anstelle des Kopierkonstruktors ausgeführt, was der Laufzeit zugutekommt. Dies gilt natürlich nur, wenn im Konstruktor keine Ausnahme ausgelöst wird. Dabei ist aber zu beachten, dass einige Operationen, wie z.B. der new Operator, selbst Ausnahmen auslösen können.
Versuchen niemals eine Klasse von einem Bibliotheks-Container abzuleiten! Die Container enthalten keinen virtuellen Destruktor und damit werden diese nicht ordnungsgemäß gelöscht, wenn das abgeleitete Objekt gelöscht wird.