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