Programmierung I Templates (generic programming)
Transcription
Programmierung I Templates (generic programming)
Das Problem Code-Duplizierung: Programmierung I Templates (generic programming) Wintersemester 2003/2004 University Bonn Dr. Gabriel Zachmann float max( float a, float b ) { return (a < b) ? b : a; } int max( int a, int b ) { return (a < b) ? b : a; } string max( string a, string b ) { return (a < b) ? b : a; } "Big No-No" !! Lösung: Templates Anatomie einer Template-Deklaration Weitere Abstraktion → Templates Funktions-Template zum Beispiel: template <typename T> const T & max( const T & a, const T & b ) { return (a < b) ? b : a; } Vorschrift, um Familie von (übeladenen) Funktionen zu erzeugen (lassen) template < … > leitet Template ein – alles Folgende kann unbestimmte Typen enthalten, erzeugt keinen Code Template-Parameter – Platzhalter für Typ (eine Art "Typ-Variable") template <typename T> const T & max( const T & a, const T & b ) { return (a < b) ? b : a; } Erzeugt noch keinen Code! ("there is no spoon" …) Template-Parameter kann überall vorkommen, wo sonst Typ stehen darf Argument Deduction Benutzung: float f1 = 1.2, f2 = 2.3; float f3 = max( f1, f2 ); int i1 = 1, i2 = 2; int i3 = max( i1, i2 ); string s1("blub"), s2("bla"); string & s3 = max( s1, s2 ); Erzeugt die Funktionen: float max( float a, float b ); int max( int a, int b ); string max( string a, string b ); Woher weiß der Compiler, welchen Typ er für T bei der Instanziierung einsetzen muß? Durch Matching der Typen: float f3 = max( 1.2, 2.3); float f4 = max( 1, 2.3); // ok, T = int // error Achtung: keine automatische Typkonvertierung bei der Deduktion! Explizite Instanziierung: float f4 = max<double>( 1, 2.3); Vorgang heißt (Template-)Instanziierung Keine Deduktion mehr! 1 Overloading Klassen-Templates Genauso wie bei non-template Funktionen: template <typename T> const T & max( const T & a, const T & b ) { return (a < b) ? b : a; } template <typename T> const T & max( const T &a, const T &b, const T &c ) { return max( max(a,b), c ); } char * max( char * a, char * b ) { if ( strcmp(a,b,) < 0 ) return b; else return a; } Spezialisierungsregion Beispiel: template <typename NodeType> class List { public: List(); void insert( const NodeType & newnode, int pos ); void append( const List & list ); protected: NodeType *head; }; template <typename NodeType> void List<NodeType>::insert( const NodeType & newnode, int pos ) { ... } Benutzung Versuch der Plausibelmachung der Syntax der Definition: template <typename NodeType> void List<NodeType>::append( const List & list ) { int n = list.length(); List l; ... } Return-Typ gehört nicht zur Spezialisierungsregion! Analog zu Funktions-Templates Beispiel: int main() { Stack<int> intStack; Stack<char*> stringStack; intStack.push(17); stringStack.push( "blub" ); Spezialisierungsregion Deklarationsblock einer Klasse ist auch Spez.-region Beachte: Identifier Stack ist sinnlos, nur Stack<typ> ist sinnvoll! Analogie zum Konzept "Klasse" Klasse = Hilfsmittel, um zur Laufzeit neue Objekte zu erzeugen Template = Hilfsmittel, um zur Compile-Zeit neue Klassen/Funktionen zu erzeugen Unterschiede bei der Instanziierung: Aus Klasse: Instanzvariablen mit verschiedenen Werten Keine Argument-Deduktion Achtung: bei geschachtelter Template-Instanziierung Stack< Stack<int> > stackOfStacks; Space! Non-Type Template-Parameter Häufiges Problem: Arrays (Instanzvariablen) sollen maximale Größe bei Erzeugung des Objektes bekommen Lösung durch Parameter beim Konstruktor hat Nachteile Größe bleibt konstant Lösung: Non-Type Template-Parameter Aus Template: "Typvariablen" mit verschiedenen konkreten Typen Generisches Programmieren (eigtl. kein oo Paradigma mehr) 2 Beispiel template <typename NodeType, int MaxSize> class List { public: List(); void insert( const NodeType & newnode, int pos ); void append( const List & list ); protected: NodeType list[MaxSize]; }; Template-Probleme 1. Debugging 2. "Code Bloat" 3. Syntax-Checking erst nach Instanziierungs möglich 4. Templates im traditionellen Comilation-Modell List<int,100> intList100; List<char*,1000> stringList; Analog für Funktionstemplates Einschränkung: nur intergale Typen zugelassen Debugging von Templates Beispiel: vector<string> v; v.push_back( 17 ); Code-Bloat List.h template <typename NodeType> class List { ... }; #include <List.h> ergibt: test.cpp: In In function function `int `int main()': main()': test.cpp: test.cpp:17: test.cpp:17: no no matching matching function function for for call call to to `vector<basic_string<char,string_char_traits<char>,__default_alloc_template<true,0> `vector<basic_string<char,string_char_traits<char>,__default_alloc_template<true,0> >,__default_alloc_template<true,0> >::push_back >::push_back (int)' >,__default_alloc_template<true,0> (int)' /usr/include/g++-2/stl_vector.h:144: candidates /usr/include/g++-2/stl_vector.h:144: candidates are: are: vector<basic_string<char,string_char_traits<char>,__default_alloc_template<true,0> vector<basic_string<char,string_char_traits<char>,__default_alloc_template<true,0> >,__default_alloc_template<true,0> >::push_back<string, >::push_back<string, alloc>(const >,__default_alloc_template<true,0> alloc>(const string string &) &) FileA.cpp ... List<int> l1; ... List<int> l1; FileC.cpp ... List<int> l1; g++ FileA.o Lesehilfe: von vorne und von hinten, bis hilfreicher Identifier auftaucht (hier: vector bzw. push_back) Syntax-Checking FileB.cpp FileB.o FileC.o g++ program Enthält 3x Code für List<int>! Templates im traditionellen Compilations-Modell Problem: Compiler kann Syntax erst checken, wenn Template instanziiert wird! Beachte: Code aus Template wird erst erzeugt, wenn tatsächlich benötigt! Beispiel: wird folgendes compilieren? Folge: folgendes Programm compiliert, aber linkt nicht! template <typename T> const T & max( const T & a, const T & b ) { return (a < b) ? b : a; } Antwort: das hängt davon ab … Folge: offensichtliche Syntax-Fehler gehen durch: template <typename T> const T & max( const T & a, const T & b ) { return blub; } foo.h template <typename T> const T & foo( const T & a ); foo.cpp #include <vector> template <typename T> const T & foo( const T & a ) { ... } main.cpp #include <foo.h> int main( ) { int i; foo( i ); } % g++ foo.cpp –o foo.o % g++ main.cpp –o main.o % g++ foo.o main.o –o prog 3 Inclusion Model Implementierung (indirekt) im Header-File: Probleme: Kosten von #include <foo.h> stark gestiegen → Große Systeme brauchen Stunden zu Compilieren foo.h template <typename T> const T & foo( const T & a ); Ältere Compiler: Beschwerde, wenn 2x foo<int> beim Linken auftaucht #include <foo.hpp> foo.hpp #include <vector> template <typename T> const T & foo( const T & a ) { ... } Explicit Instantiation Model Instanziiere alle benötigten Template-Instanzen genau 1x im Programm "von Hand" Syntax: template const int & foo<int>( const int & a ); template class Foo<int>; Beispiel: foo.cpp #include <vector> foo.h template <typename T> const T & foo( const T & a ); template <typename T> const T & foo( const T & a ) { ... } foo_inst.cpp #include <foo.cpp> template const int & foo<int>( const int & a ); template const float & foo<float>( const float & a ); Separation Model Nachteil: Schreibe export vor Template-Deklaration: Man muß jede Instanziierung "von Hand" vornehmen → Wird in großen Projekten sehr schnell nicht mehr praktikabel foo.cpp foo.h export template <typename T> const T & foo( const T & a ); main.cpp #include <vector> #include <foo.h> template <typename T> const T & foo( const T & a ) { ... } int main( ) { int i; foo( i ); } Nachteile: Nur von sehr wenigen Compilern unterstützt Funktioniert traditionelles Dependency-Checking noch? (make & Co.) Compile-Zeiten nicht notwendigerweise kürzer 4 Statische statt dynamische Polymorphie Beispiel: allg. und spezielle Klassen für Matrizen class Matrix { public: virtual double operator()(int i, int j) = 0; }; Einfacher Ansatz: Gemeinsamkeiten in Klassen-Template Spezialitäten in "Engine"-Klassen Beispiel: class SymmetricMatrix : public Matrix { public: virtual double operator()(int i, int j); }; class Symmetric { // Encapsulates storage info for symmetric matrices }; class UpperTriangular { // Storage format info for upper tri matrices }; class UpperTriangularMatrix : public Matrix { public: virtual double operator()(int i, int j); }; template < class T_engine > class Matrix { private: T_engine engine; }; Beispiel: Spur einer Matrix Größtes Problem: alle Matrix-Arten haben exakt gleiche Methoden-Menge! template<class T_engine> double trace( Matrix<T_engine>& A ) { double t = 0.0; for ( i = 1; i < n; i ++ ) for ( j = 1; j < n; j ++ ) t += engine(i,j); } Matrix<Symmetric> A; trace(A); Beispiel: template < class T_engine > class Matrix { public: bool isSymmetricPositiveDefinite() { return engine.isSymmetricPositiveDefinite(); } private: T_engine engine; }; Macht keinen Sinn für Matrix<UpperTriangular> ! "Barton & Nackman Trick" template < class T_leaftype > class Matrix { public: T_leaftype& asLeaf() { return static_cast<T_leaftype&>(*this); } double operator()(int i, int j) { return asLeaf()(i,j); } // delegate to leaf }; class SymmetricMatrix : public Matrix<SymmetricMatrix> { ... }; Vorteile: Unterklassen können eigene Methoden haben class SymmetricMatrix : public Matrix<SymmetricMatrix> { public: bool isSymmetricPositiveDefinite() { return engine.isSymmetricPositiveDefinite(); } }; Oberklasse kann gemeinsame Methoden haben Redefinition funktioniert wie gehabt class UpperTriMatrix : public Matrix<UpperTriMatrix> { ... }; 5 Zweites Beispiel: Callbacks Callbacks in C++ sind oft Funktoren Lösung: virtuelle Methode durch Template-Parameter ersetzen Beispiel früher: generische Integration class Function { public: virtual float operator () ( float x ) const = 0; }; float integrate( float a, float b, int n, const Function & f ); Nachteil: Overhead durch virtual template < class Function > float integrate( float a, float b, int n, const Function & f ) { float delta = (b-a) / (n-1); float sum = 0.0; for ( int i=0; i < n; ++ i ) sum += f(a + i*delta); return sum * (b-a) / n; } // Example use integrate( 0.3, 0.5, 30, Polynom(0,1,2) ); Template Metaprograms Beispiel: Optimierung Mit Templates kann man zur Compile-Zeit "rechnen" Beispiel: Fakultät template<int N> class Factorial { enum { value = N * Factorial<N-1>::value }; }; template<> class Factorial<1> { enum { value = 1 }; }; TemplateSpezialisierung Produkt zweier N-dim. Vektoren: double dot( const double* a, const double* b, int N ) { double result = 0.0; for ( int i=0; i < N; i++ ) result += a[i] * b[i]; return result; } Performance: // Example use const int fact5 = Factorial<5>::value; Loop-Unrolling per Metaprogramm 1. Vektor als Klasse: template <class T, int N> class TinyVector { public: T operator[]( int i ) const { return data[i]; } private: T data[N]; }; // Example use TinyVector<float,4> x; float x3 = x[3]; 2. Funktion als Template-Funktion: template<class T, int N> T dot( TinyVector<T,N> & a, TinyVector<T,N> & b ) { return meta_dot<N-1>::f(a,b); } // A length 4 vector of float // The third element of x 6 3. Das (rekursive) Metaprogramm: template < int I > struct meta_dot { template < typename T, int N > static T f( TinyVector<T,N>& a, TinyVector<T,N>& b ) { return a[I]*b[I] + meta_dot<I-1>::f(a,b); } }; template<> struct meta_dot<0> { template<class T, int N> static T f( TinyVector<T,N>& a, TinyVector<T,N>& b ) { return a[0]*b[0]; } }; Was der Compiler daraus macht: TinyVector<double,3> a, b; double r = = = = = dot(a,b); meta_dot<2>(a,b); a[2]*b[2] + meta_dot<1>(a,b); a[2]*b[2] + a[1]*b[1] + meta_dot<0>(a,b); a[2]*b[2] + a[1]*b[1] + a[0]*b[0]; Achtung! Ist nur ein einfaches Beispiel zur Illustration des Prinzips Viele Compiler können Loop-Unrolling automatisch Aber: man kann z.B. komplette FFT als Metaprogramm implementieren (s. Blitz++) 7