Standard Template Library Ralph Thesen Institut für Numerische Simulation Rheinische Friedrich-Wilhelms-Universität Bonn Seminar: Technische Numerik Dezember 2009
Überblick 1 Einleitung 2 Templates 3 STL - Grundsteine vector vs. array iterator 4 Container Sequenz-Container Assoziative Container 5 Funktoren 6 Algorithmen transform sort 7 Smart-Pointer 8 Anhang
Vorwarnung Vorwort Die STL ist kein formaler Teil des C++-Standards. Das ist aber in diesem Jahrtausend kein Problem mehr. Die STL gibt es für alle Betriebssyteme, Compiler und Architekturen, auf denen es C++ gibt. (u.a. wurde die STL geschaffen, um Container unabhängig von o.g. Bedingungen realisieren zu können.) Die STL ist in C++ für C++ geschrieben. Keine Panik! Es werden viele vollständige Beispiele gezeigt. Wir kratzen heute nur an der Oberfläche.
Templates
Was sind Templates? Wikipedia: Templates sind Programmgerüste, die bei Bedarf vom Compiler instantiiert, also als normale Klassen oder Funktionen, zur Verfügung gestellt werden. Mit anderen Worten: Templates sind (nicht nur übersetzt) Schablonen, die der Compiler bei Bedarf für uns ausmalt. Quelle: http://www.grussschablonen.de
Funktions Templates Ersetze: int max ( int x, int y) { if (x < y) return y; else return x; Durch: template < class T > T max (T x, T y) { if (x < y) return y; else return x; Alternativ zu Templates könnte man die Funktion überladen, was jedoch viel Schreibarbeit benötigen würde. template<typename T> funktioniert analog zu template<class T>.
Klassen Templates Ersetze: class intcontainer { int data ; public : void set ( int i) { data = i; int get () { return data ; ; Durch: template < class T > class Container { T data ; public : void set ( T i) { data = i; T get () { return data ; ; Wir haben uns einen Container für beliebige Klassen gebaut. Auch Container<BuntesMegaObjekt> mycontainer; klappt. Auch hier gilt: template<typename T> funktioniert analog zu template<class T>.
Templates - Ein komplexeres Beispiel template < class T, int N > class mysequence { T memblock [N]; public : void setmember ( int x, T value ); T getmember ( int x); ; template < class T, int N > void mysequence <T,N >:: setmember ( int x, T value ) { memblock [x]= value ; template < class T, int N > T mysequence <T,N >:: getmember ( int x) { return memblock [ x]; Template ist abhängig vom Datentyp T und der Anzahl N.
Templates Man kann class -Templates auch spezialisieren, so dass verschiedene Datentypen verschieden behandelt werden. Man kann Templates mit non-type Parameter bauen.... und diese spezialisieren. Wichtig ist: Erst beim kompilieren wird entschieden, was mit dem Template passiert. Aber: Um die STL zu benutzen, muss man nicht in die Tiefen der Metaprogrammierung abtauchen, denn das ist alles bereits erledigt.
STL - Grundsteine
Array Arrays kennen wir aus C, wir haben sie verflucht, und wir werden es weiterhin (sollten wir sie wirklich noch benutzen müssen). Auf Arrays greift man mit Pointern zu, mit Pointern in den Speicher und nicht immer an die Stelle, an die man auch zugreifen möchte. # include < iostream > /* const is necessary */ const int N =3; int main () { int numbers [N]; /* insert some numbers */ numbers [0]=0; numbers [1]=42; numbers [2]=3; /* output data */ std :: cout << numbers [0] << std :: endl ;
Vector Ein Sequenz-Container aus der STL. Wir benutzen ihn hier analog zu einem Array. # include < iostream > # include < vector > int main () { std :: vector <int > numbers (3); /* insert some numbers */ numbers [0]=0; numbers [1]=42; numbers [2]=3; /* output data */ std :: cout << numbers [0] << std :: endl ;
Array - Größe ändern # include <iostream > int main () { int n = 3; /* create array of size n */ int * nums = new int [n]; /* insert data */ for ( int i =0; i<n; i ++) nums [ i]=i; /* resize array */ int * more_nums = new int [ n +1]; /* copy data */ for ( int i =0; i<n; i ++) more_nums [i]= nums [i]; /* destroy the old array */ delete [] nums ; /* set pointer to new array */ nums = more_nums ; /* add data */ nums [n]=n; /* clean up */ delete [] nums ; Das ließe sich auch auf die C-Methode mit malloc, realloc und free machen, aber auch das ist gruselig.
Vector - Größe ändern Wir benutzen den <vector> und überlassen diesem das Speichermanagement. # include < iostream > # include < vector > int main () { int n = 3; /* create int vector of size n */ std :: vector <int > numbers (n); /* insert data */ for ( int i =0; i<n; i ++) numbers [i]=i; /* resize vector */ numbers. resize (n +1); /* add data */ numbers [n]=n; /* don t care about resizing : */ numbers. push_ back ( 42 ); Auch wenn wir die Größe unserer Daten kennen und nichts dynamisch ändern müssen, lohnt es sich, <vector> einzusetzen.
2D-Array # include < cassert > /* const is necessary */ const int N =200; int main () { int matrix [N][N]; /* insert some numbers */ for ( int i =0; i<n; i ++) for ( int j =0; j<n; j ++) matrix [i][j]=i*n+j; /* check numbers */ for ( int i =0; i<n; i ++) for ( int j =0; j<n; j ++) assert ( matrix [i][j] == i*n+j); Das Beispiel, wie man die Größe eines 2D-Array dynamisch ändert, bleibt erspart. Kann ein Vector auch solche Strukturen beinhalten?
2D-Vector # include <cassert > # include <vector > using namespace std ; int N =200; int main () { vector < vector <int > > matrix (N, vector <int >(N)); /* insert some numbers */ for ( int i =0; i<n; i ++) for ( int j =0; j<n; j ++) for ( int k =0; k<n; k ++) matrix [i][j]=i*n+j; /* check numbers */ for ( int i =0; i<n; i ++) for ( int j =0; j<n; j ++) for ( int k =0; k<n; k ++) assert ( matrix [i][j] == i*n+j); Ein vector kann auch einen vector beinhalten. Oder ein set. Oder eine map. Oder...
<vector> Sequenz-Container, dynamischer Array. Mit push back() kann man Elemente am Ende des Vectors hinzufügen. Mit operator[] oder at() kann man auf Elemente zugreifen. Mit size() erfährt man die Größe des Vectors, mit capacity() die Kapazität. Größe kann man mit resize() und die Kapazität mit reserve() beeinflussen. Weiterhin kann man auf einen Vector mit einem Iterator zugreifen.
iterator (1) STL Container stellen Iteratoren zur Verfügung. z.b. vector<int>::iterator i,j; Ein Iterator bezeichnet ein Element einer Sequenz. Sequenz-Container stellen begin() und end() zur Verfügung, die Iteratoren zurückgeben. Beachte: [begin:end[ Iterator kann hinter das letzte Element zeigen. i==j vergleicht zwei Iteratoren. ++i verschiebt einen Iterator zum nächsten Element. *i zeigt auf das Element selbst.
iterator - Beispiel # include < iostream > # include < vector > int main () { std :: vector <int > nums ; /* insert data */ nums. push_back (0); nums. push_back (42); nums. push_back (3); /* declare iterator */ std :: vector <int >:: iterator it; /* print vector using iterator */ for (it = nums. begin (); it < nums. end (); it ++ ) std :: cout << *it << std :: endl ; /* change the first entry using the iterator */ * nums. begin () = 1; /* change the second (=++ first ) entry */ *(++ nums. begin ()) = 2; /* print again */ for (it = nums. begin (); it < nums. end (); it ++ ) std :: cout << *it << std :: endl ;
iterator (2) Iteratoren erleichtern den Zugriff auf Container. Iteratoren funktionieren als Schnittstelle zwischen Containern, Algorithmen und dem ganzen Rest. const iterator const iterator!= const iterator iterator const iterator const iterator it++ ok ok val=*it; ok ok ok *it=val; ok ok
Container
Sequenz-Container Neben <vector> gibt es noch <deque> und <list>. <vector> und <deque> stellen Random-Access-Iteratoren (z.b. it+=5; ), <list> nur bidirektionale Iteratoren (z.b. it++ ) zur Verfügung. Jeder Sequenz-Container verspricht für bestimmte Operationen verschiedene Kosten. vector deque list bidirectional access O(1) O(1) O(1) random access O(1) O(1) O(n) prepend O(n) O(1)* O(1) append O(1) O(1)* O(1) random insert O(n) O(n) O(1) (* amortisierte Kosten)
Sequenz-Container und Iteratoren Sequenz-Container stellen die Funktionen assign, insert und erase zur Verfügung, die mit Iteratoren arbeiten. # include <iostream > # include <list > # include <vector > /* delcare a typedef */ typedef std :: vector <int >:: iterator ivec_iter ; int main () { int arr [] = {1,2,3,4; std :: vector <int > v; v. assign (arr, arr +4); /* insert something at the beginning */ v. insert (v. begin (),0); /* erase same at the end */ v. erase (v. end () -2,v. end ()); for ( ivec_iter it=v. begin (); it <v. end (); it ++) std :: cout << *it << std :: endl ; /* create a new list */ std :: list <int > l; /* assign a range from vector v to list l*/ l. assign (v. begin ()+1, v. end () -1);
In-/Output-Iteratoren In- und Output-Iteratoren sind Forward-Iteratoren. Input-Iteratoren kann man nur lesen, Output-Iteratoren nur schreiben. # include <iostream > /* std :: cout */ # include <vector > # include <iterator > using namespace std ; int main () { int arr [] = {1,2,3; /* initialize the vector using the array pointer */ vector <int > v(arr, arr +3); /* create an ostream_iterator of the type string */ ostream_iterator < string > o_i_string (cout,"\n"); /* write something to the ostream */ o_i_string =" Hello World!"; /* copy the vector <int > to an ostream_iterator <int > */ copy (v. begin (),v. end (), ostream_iterator <int >( cout,"\n" )); /* look a std :: string */ string str = " Hello World!"; /* use an iterator to access str */ string :: iterator istr = str. begin ()+6; cout << * istr << endl ;
Assoziative Container set Menge von eindeutigen Objekten. Der Wert entspricht dem Schlüssel. multiset Menge von Objekten. Der Wert entspricht dem Schlüssel. map Menge von Paaren von Schlüsseln und Werten. Die Schlüssel müssen eindeutig sein. multimap Menge von Paaren von Schlüsseln und Werten. Die Schlüssel dürfen mehrfach vorkommen. Assoziative Container unterstützen folgende Funktionen: insert() count() find() erase()
<multiset> # include <set > /* header for set and multiset */ # include < iostream > int main () { int arr [] = {1,2,3,4,2,4; /* init multiset with array values */ std :: multiset <int > ms(arr, arr +6); std :: multiset <int >:: iterator it; /* search n destroy */ it=ms. find (1); ms. erase (it ); /* without an iterator */ ms. erase (ms. find (3)); /* print values */ for (it=ms. begin (); it!= ms.end (); it ++) std :: cout << *it << std :: endl ; /* count values */ std :: cout << " count (4): " << ms. count (4) <<std :: endl ;
<map> # include <iostream > # include <map > using namespace std ; int main () { map < string, double > numbers ; // declare map map < string, double >:: iterator it; // declare iterator // declare return value pair <map < string, double >:: iterator,bool > retval ; // insert data numbers. insert ( pair < string, double >(" zero ",0.) ); numbers. insert ( pair < string, double >(" answer ",42) ); // try to insert " answer " again. retval = numbers. insert ( pair < string, double >(" answer ",41) ); if ( retval. second == false ) { cout << " answer already exists with the value " // retval. first contains an iterator pointing to the // existing pair key => value << retval. first -> second << "." << endl ; cout << " numbers :" << endl ; for ( it= numbers. begin () ; it!= numbers. end (); it ++ ) cout << (* it ). first << " => " << (* it ). second << endl ;
Funktoren
Funktionsobjekte Funktoren Ein Funktionsobjekt ist ein Objekt, das den operator() besitzt und somit mit derselben Syntax wie eine Funktion aufgerufen werden kann. # include < functional > # include < iostream > int main () { // create binary greater - functor std :: greater <int > greater ; if ( greater (4,2)) std :: cout << "4 is greater than 2." << std :: endl ; // create unary negate - functor std :: negate < double > neg ; std :: cout << neg (2) << std :: endl ;
Funktor im Eigenbau # include <functional > # include <iostream > # include <math.h> /* fabs () */ /* our new functor */ template <class T> class equal_enough : /* derive from binary_function */ public std :: binary_function <T, T, bool > { public : bool operator ()( const T &a, const T & b) const { return ( fabs (a-b) <10e -14); ; int main () { /* create equal - functor */ std :: equal_to < double > equal ; /* create my equal - enough - functor */ equal_enough < double > equal_e ; if ( equal (0.,1e -16)) std :: cout <<"0 equals 1e -16 " <<std :: endl ; if ( equal_e (0.,1e -16)) std :: cout <<"0 and 1e -16 are equal enough " <<std :: endl ;
Adapter Mit einem Adapter kann man einen Funktor aus existierenden Funktionen ableiten. Man kann ein Funktionsobjekt generieren, das eine existierende (Member-)Funktion aufruft. ptr fun() erzeugt einen Funktor für eine Funktion. z.b. std::ptr fun(&atoi) ; ptr fun ref arbeitet analog, nur dass das Objekt als Referenz übergeben wird. mem fun() erzeugt einen Funktor für eine Memberfunktion. z.b. std::mem fun(&std::string::length) mem fun ref arbeitet analog, nur dass das Objekt als Referenz übergeben wird. Die Adapter sind Template-Klassen, deren Template-Parameter vom Compiler automatisch gewählt werden.
Algorithmen
Algorithmen Algorithmen in der C++ STL unterscheiden sich von denen anderer Programmiersprachen. Normales Konzept: Jede Container-Klasse stellt gewisse Operationen (Sortieren, Suchen von Nachbarn, etc.) selbst zur Verfügung. In der STL arbeiten die Algorithmen mit Iteratoren und somit mit jedem Container, der diese zur Verfügung stellt. Um Algorithmen zu benutzen, muss man die Header-Datei <algorithm> einbinden.
transform transform arbeitet einen Bereich ab, schiebt das zu transformierende in eine Funktion und schreibt deren Ergebnis in den Zielbereich. # include <iostream > # include <vector > # include <string > # include <algorithm > /* we uses the transform - algorithm */ # include <functional > /* we uses mem_fun_ref */ using namespace std ; int main () { vector < string > num_vec ; num_vec. push_back (" standard template library "); num_vec. push_back (" stl "); /* create target vector ( and resize right ) */ vector <int > len_vec ( num_vec. size ()); /* the transform algorithm */ transform ( num_vec. begin (), num_vec. end (), /* uses a the string - member - function length */ len_vec. begin (), mem_fun_ref (& string :: length )); /* print result */ for ( unsigned int i =0; i< num_vec. size (); i ++) { cout << num_vec [i] <<" has " << len_vec [i] <<" letters." << endl ;
sort sort sortiert einen gegebenen Bereich und benutzt eine gegebene Vergleichsfunktion, im Default: less. # include <iostream > # include <vector > # include <algorithm > # include <iterator > int main () { int arr [] = {3,1,2,5,4; std :: vector <int > vec (arr, arr +5); std :: vector <int >:: iterator it; /* sort the first the elementes of vec */ std :: sort ( vec. begin (), vec. begin ()+3); /* use greater to sort the whole vextor */ std :: sort ( vec. begin (), vec. end (), std :: greater <int >()); /* btw : copy is an algorithm, too */ std :: copy ( vec. begin (), vec. end (), std :: ostream_iterator <int >( std :: cout,"\n" )); return 0;
Smart-Pointer
Smart-Pointer Speicherverwaltung kann aufwendig werden. Wenn zwei (oder mehr) Objekte Pointer auf denselben Bereich haben, woher weiß ein Objekt, dass es den Speicher freigeben kann? Die anderen Objekte erwartet weiterhin, dass der Bereich reserviert ist. delete kann nicht stumpf im Destructor ausgeführt werden. Welches Objekt gibt den Speicher frei? Wie verhindert man mehrfaches delete? Smart-Pointer sind eine elegante Lösung für diese Problematik.
auto ptr # include < memory > /* auto_ ptr lives here */ class A { public : void do_something () { /* a smart pointer as member variable */ std :: auto_ptr <int > i; ; /* the usual way for allocating an object */ void f() { A* ptr = new A; ptr -> do_something (); delete ptr ; /* here we use the smart pointer auto_ptr <> */ void g() { std :: auto_ptr <A > ptr ( new A ); ptr -> do_something (); int main () { f (); g ();
auto ptr / shared ptr / shared array std::auto ptr<> aus <memory> haben immer genau einen Besitzer. wechseln den Besitzer, wenn sie neu zugewiesen werden. können nicht in Standard-Container gespeichert werden. lassen sich nicht mit Arrays verwenden. boost::shared ptr<> aus <boost/shared ptr.hpp> verwenden Referenzzähler für das Zählen der Besitzer. werden freigegeben, wenn der letzte Besitzer seinen Gültigkeitsbereich verlässt. können in Standard-Container gespeichert werden. boost::shared array<> aus <boost/shared array.hpp> stellt Smart-Pointer für Arrays zur Verfügung.
shared ptr # include <iostream > # include <vector > # include <set > # include <boost / shared_ptr.hpp > /* dummy class */ class A { public : A( int _x) : x(_x ){; ~A (){ std :: cout <<"~A x=" <<x<< std :: endl ;; int x; ; int main () { /* we use a set and a vector of our smart pointer */ std :: vector < boost :: shared_ptr <A> > A_vector ; std :: set < boost :: shared_ptr <A> > A_set ; /* create a shared pointer and initialize */ boost :: shared_ptr <A> A_ptr ( new A( 0 ) ); A_ptr. reset ( new A( 1 ) ); /* reset to new pointer */ /* insert in vector and set */ A_set. insert ( A_ptr ); A_vector. push_back ( A_ptr ); /* */ A_ptr. reset ( new A( 2 ) ); /* reset to new pointer */ A_vector. push_back ( A_ptr ); A_set. clear (); /* clear the set, remove all pointer */ for ( unsigned int i =0;i< A_vector. size (); i ++) std :: cout << A_vector [i]->x << std :: endl ;
Anhang
Was ich nicht erzählt habe...
Literatur http://www.cplusplus.com/reference/ http://www.sgi.com/tech/stl/ Wem das zu langweilig wird http://spc.unige.ch/mptl MultiProcessing Template Library