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