11 - STL.fm

Transcription

11 - STL.fm
Modul Programmieren mit C++
Kapitel Standard Template Library
Doku
Fachhochschule Nordwestschweiz
Prof. H. Veitschegger
3r0
Seite
11.1
11 Standard Template Library
11.1 Grundkonzept
Die Standardbibliothek von C++ enthält verschiedene Komponenten, welche von Entwicklern immer wieder
benötigt werden: Einerseits trägt sie das Erbe der alten C-Standardbibliothek mit sich, welches unter anderem
aus Funktionen für die alten C-Strings, einer kleinen Mathematik-Bibliothek mit trigonometrischen
Funktionen, Logarithmen usw. und aus einer Sammlung von Funktionen für den Zugriff auf Dateien besteht.
Anderseits enthält sie aber eine Reihe von nützlichen Klassen, von welchen ein grosser Teil als Templates
implementiert sind. Diese Standard Template Library (STL) besteht im wesentlichen aus Daten-Containern,
wobei diese sogenannte Iteratoren als standardisierte Zugriffs-Schnittstelle besitzen und durch eine Vielzahl
von Algorithmen ergänzt werden, die gegen Iteratoren programmiert sind. Die STL besteht also aus drei
Komponenten-Typen, die auf bestimmte Weise zusammenarbeiten:
Container
Iteratoren
Algorithmen
• Container
Wie der Name bereits sagt, sind Container Behälter, und zwar Behälter von Daten. Sie dienen dazu, grössere
Datenmengen aufzunehmen und unterscheiden sich von den Arrays (dem Urvertreter eines Containers)
dadurch, dass sie dynamisch sind, die Implementierungsdetails dem normalen Benutzer verborgen sind, und
dass eine ganze Reihe von Funktionen zu deren Verwaltung zur Verfügung stehen. Container stehen in
verschiedenen Stilvarianten zur Verfügung. So findet man zum Beispiel array-ähnliche und listen-ähnliche
Container.
Man findet sogar Container-Adapter, die Containern das Aussehen verschiedener abstrakter Datentypen geben
können. Angebotsübersicht: Vektoren, Listen, Deques, Stacks, Queues, Priority-Queues, assoziative Container.
• Iteratoren
Im Vergleich zur Java-Bibliothek bietet die STL eine recht umfangreiche Menge verschiedenartiger Iteratoren
an, die je nach Art des Containers unterschiedlich reichhaltig mit Zugriffsfunktionen ausgestattet sind.
Iterator-Klassen werden von den Containern selbst als innere Klassen bereitgestellt. Die Container enthalten
Methoden, um Iteratoren auf den Anfang oder das Ende des Containers zurückzugeben. Die verschiedenen
Iterator-Typen: Input/Output-Iteratoren, Forward-Iteratoren, Bidirectional-Iteratoren, Random-Access-Iteratoren.
• Algorithmen
Ganz im Gegensatz zu den Algorithmen, wie sie von der Java-Bibliothek angeboten werden, sind die meisten
Algorithmen der C++ Bibliothek konsequent gegen Iteratoren programmiert. Das heisst, sie benutzen
ausschliesslich Iteratoren, um auf die Container zuzugreifen. Dies hat Vor- und Nachteile: Es ist einfacher, bei
Bedarf einen Container durch einen anderen, besser geeigneten Container auszutauschen, ohne dass sonst viel
Code angepasst werden muss. Aber unter Umständen ist dann der Zugriff auf einen Container mit Einbussen
der Performance verbunden.
Es wird zwischen zwei Arten von Algorithmen unterschieden: Solche, die den Container nur lesen, ihn also
nicht verändern und solche, die auch in den Container schreiben. Wir finden (auszugsweise) folgende Arten von
Algorithmen in der Standardbibliothek: Sequenz-Operatoren, Sortieren & Mischen, Mengenoperationen, HeapAlgorithmen, Vergleichen, Permutieren, Extremwertsuche, numerische Algorithmen.
• Weitere Elenente der Standard-Bibliothek
Ein- und Ausgabe-System, Numerik, Fehlerbehandlung (Exceptions), Speicherverwaltung, RTTI, Internationalisierung, Strings, weitere Hilfsklassen.
Modul Programmieren mit C++
Kapitel Standard Template Library
Fachhochschule Nordwestschweiz
Doku
Prof. H. Veitschegger
3r0
Seite
11.2
11.2 Container
Containertypen
Container werden als Objekte ihrer Klasse instanziert. Sie verwalten Objekte beliebigen Typs (auch andere
Container).
• vector: Vektoren zeigen ein array-artiges Verhalten. Dementsprechend kann jedes Element mit geringem
Aufwand O(1) angesprochen werden. Hingegen ist das Einfügen in der Mitte mit O(N) teuer. Entspricht in
etwa der Klasse ArrayList der Java-Bibliothek.
• list: Doppelt verkettete Liste. Einfüge- und Löschoperationen an einem beliebigen Ort sind billig, aber
der Container erlaubt nur sequentiellen Zugriff (wahlfreier Zugriff wäre teuer: O(n) ).
• deque: Deques (double ended queues) verhalten sich ähnlich wie Listen, aber Einfüge- und Löschoperationen lassen sich an beiden Enden günstig haben. Wird häufig mit Arrays implementiert.
• stack: Abstrakter Container. Es wird nur eine Schnittstelle (Adapter) eines LIFO-Containers definiert.
Die Hintergrund-Datenstruktur muss vom Benutzer selbst ausgewählt werden (hier ist ein konkreter Container anzugeben, wie beispielsweise vector oder list).
• queue: FIFO-Datenstruktur. Auch dieser Container ist als Adapter implementiert, welcher im Hintergrund
einen konkreten Container benötigt.
• set: Dieser Container-Typ simuliert eine Menge und kann im Zusammenhang mit Mengenoperationen
benutzt werden. Es existiert die Variante multiset, welche mehrere identische Elemente erlaubt.
• map: map simuliert ebenfalls Mengen, aber Mengen von Paaren. Jedes Element besteht also aus einem
assoziierten Paar von Werten bestimmter Datentypen, wobei eine Element als Suchschlüssel benutzt wird.
Es existiert eine Variante multimap, welche mehrere Elemente gleichen Schlüssels erlaubt.
Exportierte Datentypen
Jeder Container stellt eine Reihe von Datentypen zur Verfügung. Darunter natürlich den Elementtyp und den
Typ seines Iterators. Es sei v ein Container (zum Beispiel vector<int>). Sie können folgende Typen erhalten:
v::value_type
v::reference
v::const_reference
v::iterator
v::const_iterator
v::difference_type
v::size_type
v::allocator_type
//
//
//
//
//
//
//
//
Elementtyp des Containers
Referenz auf den Elementtyp
Referenz auf eine nicht änderbare Element-Instanz
Iterator auf ein Element des Containers
Iterator auf ein konstantes Element
Abstand zwischen zwei Elementen
Grösse oder Kapzität des Containers
Objekt, dass die Speicherverwaltung des Containers übernimmt
Leider können die exportierten Typen nicht in einer intuitiv sinnvollen Art und Weise benutzt werden.
Angenommen, es sei ein Vektor definiert als vector<int> v. Dann wäre folgender Code wünschenswert,
v::value_type i;
um ein Element abhängig vom Container zu bekommen, aber leider ist nur folgender Code möglich:
vector<int>::value_type i;
Dies schränkt die Nützlichkeit des exportierten Typs erheblich ein, da der Elementtyp immer mit angegeben
werden muss. Der Entwickler von Algorithmen wird stattdessen am ehesten generische Typen benutzen.
Allgemeine Container-Methoden
Jeder Container-Typ stellt 17 verschiedene Methoden zur Verfügung, von welchen hier einige erwähnt werden
sollen. Wenn Sie eigene Container bauen wollen, sollten Sie sicherstellen, dass Sie sowohl die oben genannten
Typen, als auch diese Methoden implementieren.
Interessant sind speziell die beiden Methoden begin() und end(), die es erlauben, Iteratoren zu initialisieren.
Modul Programmieren mit C++
Kapitel Standard Template Library
Doku
Fachhochschule Nordwestschweiz
v()
v(const v&)
~v()
iterator begin()
iterator end()
size_type size()
bool empty()
//
//
//
//
//
//
//
Prof. H. Veitschegger
3r0
Seite
11.3
Default-Konstruktor
Copy-Konstruktor
Destruktor
Position des ersten Elements
Position nach dem letzten Element
aktuelle Grösse des Containers
Test auf Grösse = 0 ( begin() == end() )
Sequenzen-Methoden
Container, die ihre Elemente linear anordnen und einen sequentiellen Zugriff erlauben, können als Sequenzen
benutzt werden (vector, list, deque). Hashtabellen oder binäre Bäume sind Beispiele nicht-sequentieller
Container. Sequenzen stellen zusätzliche Methoden zur Verfügung (Ausschnitt):
v(n,t)
// Konstruktor: n Elemente mit Wert t einfügen
iterator insert(p,t)
// vor der Stelle p wird eine Kopie von t eingefügt
void insert(p,n,t)
// vor der Stelle p werden n Kopien von t eingefügt
iterator erase(q)
// Löscht das Element, das durch den Iterator q referenziert wird.
// Rückgabe: Nachfolger des gelöschten Elements.
iterator erase(q1,q2)
// Löscht alle Elemente im Bereich q1..q2 (beides Iteratoren). Das
// letzte Element (*q2) wird nicht gelöscht. Rückgabe: q2
void clear()
// alle Elemente löschen. Auch mit erase(begin(), end()) möglich.
Spezielle Methoden für vector , list und deque
void assign(iterator i, iterator j)
// Container löschen und die Elemente aus dem Bereich
// i..j einfügen
reference front()
// Referenz auf das erste Element
reference back()
// Referenz auf das letzte Element
void push_back(t)
// Element t am Ende des Containers einfügen
void push_front(t)
void resize(n, t=T())
// Element am Anfang des Containers einfügen (nur deque)
// Container-Grösse ändern. Freie Plätze mit t belegen
Spezielle Methoden für vector (Random-Access-Container)
reference operator[](n)
// Referenz auf das Element n wird zurückgegeben.
reference at(n)
// dito, aber mit Bereichsprüfung (Ausnahme)
void reserve(n)
// Speicherplatz reservieren (Verschiebt Allokationsprobleme)
size_type capacity()
// Reservierte Grösse (Kapazität des Containers)
Spezielle Methoden für list (ausschnittsweise)
void merge(list&)
// Sortierte Listen mischen
void push_front(const T& t)
// Element am Anfang einfügen
void pop_front()
// erstes Element löschen
void remove(const T& t)
// Alle Elemente mit (e==t) löschen
void reverse()
// Reihenfolge der Elemente umkehren
void sort()
// Liste sortieren: O(nlogn)
void unique()
// Duplikate entfernen
Für Sortier- und ähnliche Operationen müssen Elemente miteinander verglichen werden. Dies setzt voraus,
dass auf dem Elementtyp ein Vergleichsoperator operator< definiert ist (dies gilt insbesondere für selbstgebaute Klassen und Datentypen).
Modul Programmieren mit C++
Kapitel Standard Template Library
Fachhochschule Nordwestschweiz
Doku
Prof. H. Veitschegger
3r0
Seite
11.4
Spezielle Methoden für deque (ausschnittsweise)
reference operator[](n)
// Index-Operator ohne Bereichsprüfung
reference at(n)
// dito mit Bereichsprüfung
void push_front(const T& t)
// Element am Anfang einfügen
void pop_front()
// erstes Element löschen
11.3 Abstrakte Container
Die Containertypen stack, queue und priority-queue werden nicht als eigene Container neu implementiert, sondern mit sogenannten Adaptern realisiert. Das heisst: Die STL benutzt einen geeigneten bestehenden Container und steckt den Adapter auf.
Stack
Es dürfen alle Container benutzt werden, welche die Methoden back(), push_back() und pop_back() unterstützen. Beispiel für die Deklaration (deque ist Default):
stack<int, vector<int> >
// beachten Sie den Leerschlag zwischen den letzten beiden Zeichen!
stack stellt folgende API zur Verfügung:
bool empty()
size_type size()
reference top()
void push(const value_type& x)
void pop()
Queue
Arbeitet gut mit list und deque zusammen. API:
bool empty()
size_type size()
reference front()
reference back()
void push(const value_type& x)
void pop();
11.4 Assoziative Container
Implementation von verschiedenen Mengen-Typen (allerdings intern sortiert, um die Zugriffszeit zu verkürzen). Wir beschränken uns auf eine kurze Einführung in den Container-Typ set. Weitere Typen sind in der
Fachliteratur bestens dokumentiert. Um die Elemente sortieren zu können, muss ein Vergleichsoperator
definiert sein. Ein eigenes Vergleichskriterium kann bei der Konstruktion der Menge mitgegeben werden.
Konstruktoren:
set()
set(c)
set(i,j,c)
set(i,j)
//
//
//
//
leerer Container, Default-Vergleich
leerer Container, Vergleichsobjekt c
Container mit den Elementen aus dem Iterator-Bereich i..j, sortiert nach c
mit Standard-Sortierung
Einige set-Methoden:
iterator insert(p,t)
iterator erase(q)
iterator find(k)
size_type count(k)
//
//
//
//
Rückgabe: eingefügtes Element
Rückgabe: Nachfolger
falls nichts gefunden: Rückgabe = end()
Anzahl der Elemente
• p und q sind Iteratoren, t ein Element und k der Schlüssel eines Elements.
Modul Programmieren mit C++
Kapitel Standard Template Library
Fachhochschule Nordwestschweiz
Doku
Prof. H. Veitschegger
3r0
Seite
11.5
11.5 Iteratoren
Konzept
Iteratoren werden als eine Verallgemeinerung von Zeigern aufgefasst. Das heisst, sie sind wie Zeiger zu
verwenden, aber sie verfügen darüber hinaus über weitere Eigenschaften. Ein zeigerähnliches Verhalten zeigen
Iteratoren durch die zur Verfügung gestellten Operatoren:
bool operator==(const Iterator<T>&) const;
bool operator!=(const Iterator<T>&) const;
Iterator<T>& operator++();
Iterator<T> operator++(int);
T& operator*() const;
T* operator->() const;
Dies ist nur die grundlegende Funktionalität eines Iterators. Jeder spezielle Iterator-Typ bietet weitere
Methoden an (zum Beispiel zum rückwärts Durchlaufen eines Containers). Iteratoren verhalten sich auch
bezüglich Initialisierung wie Zeiger: Sie können genauso vom Container getrennt erzeugt werden, wie ein
Zeiger getrennt vom Objekt erzeugt werden kann. In beiden Fällen ist der Zeiger (Iterator) uninitialisiert und
kann erst dereferenziert werden, wenn er einen vernünftigen Wert zugewiesen bekommen hat.
• Neu in C++ 11: Move-Iteratoren
Input-Iteratoren
Input-Iteratoren werden zusammen mit Eingabe-Streams eingesetzt. Sie können nur verwendet werden, um
einen sequentiellen Datenstrom zu lesen (es ist beispielsweise nicht möglich, Daten zu schreiben oder sich eine
Position zu merken). Hingegen funktionieren die Operatoren * und ++ bestens. Hinzu kommen natürlich die
beiden Operatoren << und >>. Deklaration: istream_iterator<T>
Er liest von einem Zeichenstrom und interpretiert ihn entsprechend dem angegebenen Datentyp T. Dabei zeigt
er die üblichen Schwächen: Nebst Tabulator- und Zeilenende-Zeichen interpretiert er auch Leerschläge als
Trennzeichen. Beispiel:
#include <fstream>
#include <iterator>
#include <string>
void main(){
ifstream quelle("t.txt",ios::nocreate);
istream_iterator<string,ptrdiff_t> pos(Quelle), ende;
if (pos == ende)
cout << "Datei nicht gefunden" << endl;
else{
while (pos != ende){
cout << *pos++ << endl;
}
}
}
• Man beachte, dass für Iteratoren, die nicht direkt aus einem Container stammen, der Standard-Header
<iterator> eingebunden werden muss.
• Der Iterator ende wird als Ende-Marke verwendet. Da der Wert für die Position Ende (ein Zeichen nach
dem letzten Zeichen) für alle Iteratoren gleich ist, ist es nicht nötig, ihn mit einem konkreten Datenstrom
zu verbinden.
• Der Eintrag ptrdiff_t (Standardtyp der STL) bezeichnet den Distanztyp, der verwendet wird, um den
Abstand zweier Iteratoren anzugeben. In gewissen Fällen (grosse Container) muss er angepasst werden. In
unserem Fall ist er nicht weiter interessant, muss aber angegeben werden, da dies die Definition des
Templates für istream-Iteratoren vorschreibt: template <class Iteratortyp, class Entfernungstyp>
Modul Programmieren mit C++
Kapitel Standard Template Library
Fachhochschule Nordwestschweiz
Doku
Prof. H. Veitschegger
3r0
Seite
11.6
Output-Iteratoren
Zum Input-Iterator auf Stream-Basis existiert erwartungsgemäss ein Gegenstück, das den Ausgabeoperator <<
zur Verfügung stellt. Von einem Output-Iterator kann nicht gelesen, sondern nur geschrieben werden. Um die
Ausgabe etwas besser gestalten zu können, erlaubt ein Output-Iterator, eine Trennzeichenfolge zu definieren,
die vom Iterator zwischen zwei Elemente des Typs T geschrieben wird.
Forward-Iterator
Die restlichen Iterator-Typen unterscheiden sich von den vorhergehenden dadurch, dass sie sowohl lesen als
auch schreiben dürfen. Der Forward-Iterator kann einen Container vorwärts durchlaufen, also ist der Operator
++ definiert. Diese Kategorie kommt beispielsweise in einfach verketteten linearen Listen zum Zug.
Bidirectional-Iterator
Der bidirektionale Iterator verfügt zusätzlich zum Forward-Iterator über die Eigenschaft, mit dem -- Operator
rückwärts durch den Container zu laufen. Die STL bietet zusätzlich zwei Adapter an, welche den Adressraum
des Containers umdrehen, so dass er mit dem Operator ++ rückwärts durchlaufen werden kann:
reverse_bidirectional_iterator
reverse_iterator
Random-Access-Iterator
Schliesslich gibt es noch den Iterator, der zusätzlich über den []-Operator verfügt, und des weiteren alles kann,
was der bidirektionale Iterator zu leisten vermag. Der Random-Access-Iterator stammt somit konzeptionell
vom Index des Arrays ab.
11.6 Algorithmen
Die meisten Algorithmen besitzen Iteratoren in ihren Parameterlisten, da sie nur über Iteratoren auf die
Container zugreifen. Damit sie korrekt arbeiten können, müssen die Elemente des Containers gewisse, vom
Algorithmus abhängige Bedingungen erfüllen. Dies betrifft beispielsweise Vergleichs- und Zuweisungsoperatoren.
Sequenz-Algorithmen
Sequenz-Algorithmen arbeiten alle oder einen Teil der Elemente in einem Container ab.
Einige Sequenz-Algorithmen, die den Container nicht verändern:
for_each(InputIterator first, InputIterator last, Function f)
// alle Elemente bearbeiten
find(InputIterator first, InputIterator last, const Type& val)
// ein Element suchen
count(InputIterator first, InputIterator last, const Type& val)
// Elemente zählen
equal(InputIterator1 f1, InputIterator1 l1, InputIterator2 f2)
// Container vergleichen
search(...)
// Sequenz suchen
Einige Sequenz-Algorithmen, die den Container verändern können:
copy(InputIterator f, InputIterator l, OutputIterator destBeg)
// Sequenzen kopieren
swap(Type& left, Type& right)
// Sequenzen tauschen
replace(ForwardIterator f, ForwardIterator l, Type& old, Type& neu) // Sequenz ersetzen
fill(ForwardIterator f, ForwardIterator l, const Type& val)
// Werte vorbelegen
remove(ForwardIterator f, ForwardIterator l, const Type& val)
// Elemente entfernen
unique(ForwardIterator f, ForwardIterator l)
// Duplikate entfernen
rotate(ForwardIterator f, ForwardIterator m, ForwardIterator l)
// Elemente verschieben
random_shuffle(RandomAccessIterator f, RandomAccessIterator l)
// durcheinanderbringen
partition(BidirectionalIterator first, ...)
// in Bereiche zerlegen
Modul Programmieren mit C++
Kapitel Standard Template Library
Fachhochschule Nordwestschweiz
Doku
Prof. H. Veitschegger
Seite
11.7
3r0
In C++ 11 sind 18 neue Algorithmen hinzugekommen, und swap wurde verbessert.
Name
move
move_backward
all_of
any_of
none_of
find_if_not
copy_if
copy_n
iota
minmax
minmax_element
partition_copy
is_partitioned
partition_point
is_sorted
is_sorted_until
is_heap
is_heap_until
Arbeitsweise
Sequenzen verschieben, statt kopieren
Sequenzen umgekehrt verschieben
true, wenn ein Prädikat für alle Elemente erfüllt ist
true, wenn ein Prädikat für mindestens ein Element erfüllt ist
true, wenn ein Prädikat für kein Element erfüllt ist
finde das erste Element, für das eine Bedingung nicht erfüllt ist
kopieren mit Filterung
kopiere n Elemente
zuweisen aufsteigender Werte zu den Elementen
gleichzeitig Minimal- und Maximalwert
gleichzeitig Iteratoren auf Minimum und Maximum
Kopiert jedes Element entweder ins eine oder andere Ziel
Prüft Container auf saubere Trennung anhand eines Prädikats
findet den Trennungspunkt einer Partition
ist der Container sortiert?
findet das erste, nicht korrekt einsortierte Element
erfüllen alle Elemente im Container die Heap-Bedingung?
findet das erste Element, das die Heap-Bedingung verletzt
Typ
Move-Semantik
Test auf ein Prädikat
Allgemeines
Datenstrukturen
• Der Algorithmus swap kann nun auch Arrays und Objekte des Typs array vertauschen, sofern sie gleich
gross sind.
• Neue Klasse tuple (Verallgemeinerung von pair). Kann beliebig viele Werte unterschiedlichen Typs
kombinieren:
tuple<char, Fraction*, double> t(’\n’, Fraction(3,4), 3.14);
auto bruch = get<1>(t);
// Element holen
get<2>(t) = 2.718282;
// Element setzen
std::cout << tuple_size<decltype(t)>::value; // Anzahl der Element
Stellvertretend für die im Kapitel Algorithmen Liste von Operationen, sehen wir uns einige davon etwas
genauer an: Der Algorithmus for_each wird verwendet, um mit allen Elementen eines Containers (oder einer
Sequenz daraus) die gleiche Funktion auszuführen. Die Signatur von for_each (siehe Seite 6) zeigt, was wir
alles dafür benötigen. Wie in der STL üblich, ist for_each als Funktions-Template konstruiert. Es muss mit
zwei Elementen parametriert werden:
• Eine Iterator-Klasse, deren Objekte als Schnittstellen zu einem Container verwendet werden.
• Einen Funktor, eine Funktion oder ein Funktionszeiger (Siehe auch Kapitel 11.7)
#include <algorithm>
#include <vector>
#include <iostream>
void anzeige(int x){
cout << x << ’ ’;
}
// eine Funktion zur Anzeige eines Werts
Modul Programmieren mit C++
Kapitel Standard Template Library
Fachhochschule Nordwestschweiz
Doku
Prof. H. Veitschegger
3r0
Seite
11.8
class Incr{
// eine Funktor-Klasse zum Inkrementieren eines Wertes
public:
Incr(int i=1): m_betrag(i){}
void operator()(int& x){
x += m_betrag;
}
// dieser Operator macht die Klasse zur Funktor-Klasse
private:
int m_betrag;
};
Zunächst wurden die geeigneten Funktionsobjekte definiert. Sie sehen zwei Varianten: Eine mit einer normalen
Funktion und eine mit einer Klasse, die Funktionsobjekte zur Verfügung stellt. Folgender Code wendet die
Funktions-Elemente mit dem for_each Algorithmus an:
vector<int> v(5,0);
for_each(v.begin(), v.end(), anzeige);
cout << endl;
// fünf Elemente mit dem Wert 0
// Anzeige: 0 0 0 0 0
for_each(v.begin(), v.end(), Incr(2));
for_each(v.begin(), v.end(), anzeige);
// alle Elemente um 2 inkrementieren
// Anzeige: 2 2 2 2 2
Incr i(7);
for_each(v.begin(), v.end(), i);
for_each(v.begin(), v.end(), anzeige);
// Funktor separat instanzieren
// alle Elemente um 7 erhöhen
// Anzeige: 9 9 9 9 9
Wir können nun einige weitere Algorithmen einsetzen, die ähnlich aufgebaut sind:
vector<int>::iterator i;
i = find(v.begin(), v.end(), 7);
// suche im ganzen Container nach dem Wert 7
Sortieren und Mischen
Der Standard-Sortier-Algorithmus sort() ist wie stable_sort() sehr einfach zu verwenden. Mit dem
Unterschied, dass letzterer eine partielle Ordnung auf vorsortierten Elementen gleichen Schlüssels nicht
verändert (stabile Sortierung).
template <class RandomAccessIterator>
void sort(RandomAccessIterator first, RandomAccessIterator last);
Beispiel 1: Einen Container sortieren:
vector<int> vec();
for (i=0; i<100; ++i)
vec.push_back(rand()); // rand() gehört zu CStdlib (sollte mit srand() initialisiert werden)
sort(vec.begin(), vec.end());
Beispiel 2: Zwei sortierte Container mischen (beispielsweise für Merge-Sort):
vector<int> v1, v2;
for (int i=0; i<SIZE1; ++i) v1.push_back(rand());
for (int i=0; i<SIZE2; ++i) v2.push_back(rand());
vector<int> result(v1.size() + v2.size());
sort(v1.begin(), v1.end());
sort(v2.begin(), v2.end());
merge(v1.begin(), v1.end(), v2.begin(), v2.end(), result.begin());
Modul Programmieren mit C++
Kapitel Standard Template Library
Fachhochschule Nordwestschweiz
Doku
Prof. H. Veitschegger
3r0
Seite
11.9
Mengen-Operationen
Mit Mengen-Containern können elementare mengentheoretische Operationen ausgeführt werden: Teilmengen,
Vereinigungsmenge, Schnittmenge, Differenz und symmetrische Differenz. Die entsprechenden Algorithmen
arbeiten mit Iteratoren:
bool includes(InputIterator F1, InputIterator L1, InputIterator F2, InputIterator L2)
set_union(...)
set_intersection(...)
set_difference(...)
set_symmetric_difference(...)
Beispiel:
#include <algorithm>
#include <set>
void main(){
int v1[] = {1,2,3,4};
int v2[] = {0,1,2,3,4,5,7,99,13}
int v3[] = {-2,5,12,7,33}
set<int> s1(v1,v1 + 4);
set<int> s2(v2,v2 + 9);
set<int> s3(v3,v3 + 5);
set <int> result;
set_union(s1.begin(), s1.end(), s3.begin(), s3.end(), inserter(result,result.begin()));
}
Die Resultatemenge muss mit der Funktion inserter() in die zunächst leere Resultatemenge eingebracht
werden (Ein normaler Input-Iterator reicht nicht aus).
Modul Programmieren mit C++
Kapitel Standard Template Library
Fachhochschule Nordwestschweiz
Doku
Prof. H. Veitschegger
3r0
Seite
11.10
11.7 Funktionale Elemente von C++
C++ kennt verschiedene Elemente mit funktionsartigem Verhalten:
•
•
•
•
•
•
Funktionen (seit C). Global oder als Methode. Default-Parameter oder variable Anzahl Parameter möglich.
Funktionszeiger (seit C). Gilt seit C++ 11 als deprecated. Kommen aber in Betriebssystem-APIs häufig vor.
Funktoren (seit C++). Instanzen von Klassen, die den operator() überladen.
Zeiger auf eine Methode (seit C++).
Funktionsobjekt (seit C++). Instanz der Klasse functional (#include <functional>).
Lambda-Ausdrücke (seit C++ 11). Anonyme Funktion
Funktionszeiger
Mit Funktionen können in C++ zwei Dinge getan werden:
1. Sie können ausgeführt werden. Dazu schreibt man den Namen der Funktion, gefolgt von der Parameterliste.
2. Sie können als Parameter an eine andere Funktion übergeben oder allgemein als Variablen behandelt
werden. In diesem Fall werden sie ohne Klammern und Parameterliste benutzt. Konzeptionell handelt es
sich dabei um einen Zeiger auf die Funktion. In den Beispielen wurden anzeige als Funktionszeiger und
das Incr-Objekt als Funktor an den Algorithmus übergeben.
Man kann auch Funktionszeiger-Variablen bzw. Typen definieren und zuweisen:
void (*fz)(int) = anzeige; // F-Zeiger-Variable fz deklarieren und mit anzeige initialisieren
fz(45);
// Funktion aufrufen
typedef void (*myFunc)(int); // eigenen Funktionszeigertyp myFunc definieren
Funktoren
Beispiel, siehe Seite 11.8. Durch überladen des Funktions-Operators kann sich ein Objekt wie eine Funktion
verhalten. Der Vorteil gegenüber eine einfachen Funktion liegt darin, dass ein Funktor ein Gedächtnis hat
(Attribute) und auch sonst flexibler ist (Konfektionieren und manipulieren mit anderen Methoden usw).1
Funktionsobjekte
Beispiel:
#include <functional>
int main(){
function<float (float a, int x)> func;
// Deklaration des Funktionsobjekts
vector<int> v{1, 2, 3, 4, 5};
func = ...
// Definition des Funktionsobjekts (Funktor, Funktionszeiger, Lambda, usw.)
float r = accumulate(v.cbegin(), v.cend(), 1.0f, func);
// Einsatz des Funktionsobjekts
return 0;
}
Lambda-Ausdrücke
Der Vorteil von Lambda-Ausdrücken liegt darin, dass man eine Funktion genau dort definieren kann, wo sie
benötigt wird. In dieser Hinsicht lassen sie sich mit den anonymen Klassen von Java vergleichen. Lambdas
werden häufig im Zusammenhang mit Algorithmen eingesetzt, da sie oft einfache Funktionen als Parameter
benötigen.
Syntax: Zugriffsdeklaration Parameterliste [Rückgabetyp] Funktionsrumpf
Beispiel:
vector<int> = ....
int limit = 1200;
cout << count_if(begin(v), end(v), [limit](int i) {return i < limit;});
1. Es sei anzumerken, dass auch gewöhnliche Funktionen mit einem Gedächtnis ausgestattet werden können: Eine in einer Funktion definierte statische
Variable wird nur einmal angelegt, und ihr Wert bleibt über Funktionsaufrufe hinweg erhalten. Also: Sichtbarkeit lokal (nur innerhalb der Funktion),
Lebensdauer global (im gesamten Programm).
Modul Programmieren mit C++
Kapitel Standard Template Library
Fachhochschule Nordwestschweiz
Doku
Prof. H. Veitschegger
3r0
Seite
11.11
• Zugriffsdeklaration: Ein Lambda ist immer in eine Umgebung eingebettet. Mit der Zugriffsdeklaration
können Sie festlegen, welche Variablen aus der Umgebung für den Lambda sichtbar sein sollen. Sie wird
immer mit eckigen Klammern umschlossen. Syntax im Detail:
•
•
•
•
•
•
[x]
[&x]
[=]
[&]
[=, &x]
[x, &y]
call by value
call by reference
alle Variablen sichtbar, Zugriff: call by value
alle Variablen sichtbar, Zugriff: call by reference
alle Variablen by value, x by reference
x: by value, y: by reference
Statische und globale Variablen sowie Attribute des eigenen Objekts sind immer sichtbar.
• Parameterliste und Funktionsrumpf: Wie bei normalen Funktionen
• Rückgabetyp: Kann angegeben werden oder nicht. Der Compiler kann selbst den Typ herausfinden
(anhand der return-Anweisung). void ist auch erlaubt.
Lambdas müssen nicht anonym bleiben. Man kann sie beispielsweise in einem Funktions-Objekt speichern:
#include <functional>
:
:
function<int(int)> addOffset(int offset){
return [offset](int n){ return n + offset; };
}
Modul Programmieren mit C++
Kapitel Standard Template Library
Fachhochschule Nordwestschweiz
Doku
Prof. H. Veitschegger
3r0
Seite
11.12
11.8 Ausblick
Wir haben nur einen kleinen Teil der Vielfalt, welche die STL zu bieten hat, vorgestellt. Die folgende Liste gibt
einen kurzen Überblick, was die STL an weiteren Geheimnissen bereithält:
•
•
•
•
•
Algorithmen mit binären Bäumen
Erzeugung von Permutationsfolgen
Summation in Containern
Skalarprodukt zweier Container
Partialsummen
Die STL versteht sich als Sammlung von Komponenten, die dazu geeignet sind, daraus weitere, mächtigere
Komponenten zu bauen. Für Ideen in dieser Richtung empfehle ich eines der zahlreichen Bücher, welche die
STL entweder beschreiben (für STL-Anfänger) oder sie nutzen, um Komponenten zu erstellen (für
Fortgeschrittene).
Geschichte der STL
Alexander Stepanov befasste sich während der 80er Jahre intensiv mit generischer Programmierung und
Projekten für generische Bibliotheken. Bereits 1987 entstand eine erste Bibliothek für die Programmiersprache
Ada. Da Ada aber stets eine militärische Insellösung blieb, schwenkte Stepanov auf C++ um und entwickelte
Anfang der 90er Jahre bei HP die Standard Template Library, welche er 1993 dem ISO-Standardisierungskommitee mit Erfolg vorlegte. Die STL ist Bestandteil des 1998 verabschiedeten C++ Standards.
Im Jahr 2003 wurde die STL (und auch C++) nachgebessert: ISO/IEC 14882:2003
Schwächen der STL
• Compiler-Fehlermeldungen im Zusammenhang mit STL-Elementen sind häufig lang und schwer
verständlich.
• Unvorsichtige Benutzung von STL-Containern kann zu aufgeblähtem Code führen (generelles Problem bei
Templates).
• Umfangreiche implizite Template-Instanzierungen führen zu unnötig langen Übersetzungszeiten.
Beinahe-Container
Neben den in Kapitel 11.2 vorgestellten Containern, welche alle Bedingungen für die Zusammenarbeit mit
Iteratoren erfüllen und welche ein Standard-Set von Methoden zur Verfügung stellen, bietet die STL einige
weitere Klassen-Templates an, welche im weitesten Sinne als Container aufgefasst werden können, aber
individuelle Beschränkungen und Erweiterungen aufweisen, so dass sie nicht unbedingt mit den StandardContainern vertauscht werden können:
basic_string: Basis für die Klasse string (string ist also eine Instanz von basic_string). Random-Access-
Iteratoren und Index-Zugriff sind verfügbar. Die Auswahl an Elementtypen ist beschränkt, und der Container
ist optimiert für Operation auf Zeichenketten.
valarray: Das Klassen-Template valarray stellt einen Vektor-ähnlichen Container zur Verfügung, der für
numerische Operationen optimiert ist. Deshalb sind einige nützliche numerische Operationen verfügbar (sog.
Vektor-Arithmetik), jedoch fehlen die meisten Standard-Container-Operationen. Lediglich size() und der
Index-Operator werden angeboten. Ein Zeiger auf valarray kann als Random-Access-Iterator benutzt werden.
valarray-Vektoren können mit numerischen Datentypen instanziert werden. Sie eignen sich, um Matrizen
beliebiger Dimensionalität zu implementieren (Slices).
bitset: Bitsets dienen der platzsparenden Verwaltung von booleschen Werten (sog. Flags) mit einem Speicher-
bedarf von einem Bit pro Flag. Ein Bitset beliebiger fester Grösse kann durch entsprechende Konstruktoren
definiert werden. Es stehen Operatoren zur Verfügung, die Manipulationen einzelner Flags erlauben: Bitshifting, Bitmasking, Testen einzelner Bits.
Modul Programmieren mit C++
Kapitel Standard Template Library
Fachhochschule Nordwestschweiz
Doku
Prof. H. Veitschegger
3r0
Seite
11.13
Neues Pakete in C++ 11
• Multithreading (<thread>, <future>, <mutex>, <atomic> ...)
• Reguläre Ausdrücke (<regex>)
• Rechnen mit Einheiten (<ratio>) Zum Beispiel, um die Plausibilität einer Formel zu abzuschätzen.
• Wahrscheinlichkeitsverteilungen (<random>)
11.9 Alternativen zur STL
Unvollständigkeit (z.B. fehlende Unterstützung für Concurrency) und andere, oben erwähnte Nachteile
führten zur Entwicklung alternativer Bibliotheken für C++. Es seien einige bekanntere kurz umrissen.
Boost
Open Source Bibliothek, bestehend aus mehr als 80 individuellen Teilen. Es existieren Lizenzen für offene und
proprietäre Software-Entwicklung. Bereits 1999 wurden erste Versionen dieser Bibliothek entwickelt. Boost
nutzt die Technik der Template-Programmierung intensiv. Übersicht der Pakete:
•
•
•
•
•
•
•
•
•
•
•
•
•
Verarbeitung von Text (Internationalisierung, reguläre Ausdrücke, usw.)
Container, Iteratoren und Algorithmen
Funktoren
Unterstützung generischer Programmierung. Template-Metaprogrammierung
Nebenläufige Programmierung (concurrency)
Mathematik
Testen von Software
weitere Datenstrukturen
Bildverarbeitung
Ein- und Ausgabe
Schnittstellen zu anderen Programmiersprachen
Speicherverwaltung (z.B. smart pointers)
Gerüste für verschiedene objektorientierte Entwurfsmuster
LEDA
(Library of Efficient Data types and Algorithms)
Seit 1988. Proprietär. Ab 2001 Weiterentwicklung durch Algorithmic Solutions Software GmbH. Freie (abgespeckte) Lizenz erhältlich. Ist spezialisiert auf Algorithmen für Graphen, Kompression, Kryptologie,
Geometrie.
CGAL (Computational Geometry Algorithms Library)
Seit 1996. Offene und proprietäre Lizenzen möglich. Spezialisiert auf Computer-Geometrie:
•
•
•
•
Algebra
Konvexe Hüllen, Polygone, Triangulation, Mesh
Geometrische Such- & Interpolations-Algorithmen
Formen-Analyse
Weitere Varianten zur C++ Standard-Bibliothek
• libstdc++ (GNU Standard-Bilbiothek)
• Dinkumware Standard Library
• RogueWave Standard C++ Library