C++ Metaprogrammierung

Werbung
Fachhochschule Wiesbaden
- Bachelor Allgemeine Informatik -
Prof. Dr. Karl-Otto Linn
Fachseminar
C++ Metaprogrammierung
von Sebastian Otte
Entwurf
(letzte Änderung 25. Februar 2009)
Dieses Dokument entstand im Rahmen der Fachseminarveranstaltung des 5. Semesters im Studiengang Bachelor Allgemeine Informatik an der Fachhochschule Wiesbaden.
Als Motivation für das Thema C++ Metaprogrammierung diente die Arbeit [Arc01]
von Tomas Arce, der in dieser eindrucksvoll zeigt, wie mit Hilfe von C++ Templates
Vektorrechnungen erheblich beschleunigt werden können.
Die folgendee Ausarbeitung soll einen Überblick über das Thema C++ Metaprogrammierung liefern, von den Grundlagen bis hin zu fortgeschrittenen Konzepten. Dabei erhebt
das Dokument keinen Anspruch auf Vollständigkeit und optimale Implementierungen
der gegebenen Konzepte. Alle in diesem Dokument dargestellten Quellcodes wurden mit
dem GNU-C++ Compiler (GCC) Version 3.4.2. übersetzt bzw. entsprechend getestet.
C++ Metaprogrammierung
Inhaltsverzeichnis
Inhaltsverzeichnis
1 Einführung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.1 Der Begriff Metaprogrammierung . . . . . . . . . . . . . . . . . . . . . . .
1.2 Metaprogrammierung mit C++ . . . . . . . . . . . . . . . . . . . . . . . .
2 Grundlagen . . . . . . . . . .
2.1 Ein Wort zu Klassen . . . .
2.1.1 Der Begriff Klass . . . .
2.1.2 Anonyme Klassen . . .
2.1.3 Innere anonyme Klassen
2.2 C Makros . . . . . . . . . .
2.2.1 Einfache Makros . . . .
2.2.2 Mehrzeilige Makros . .
2.2.3 Parametersubstitution .
2.2.4 Variadic Macros . . . .
2.2.5 Der #-Operator . . . . .
2.2.6 Der ##-Operator . . . .
2.2.7 Verschachtelte Makros .
2.2.8 Turing-Vollständigkeit .
2.3 C++ Templates . . . . . . .
2.3.1 Funktions-Templates . .
2.3.2 Klassen-Templates . . .
2.3.3 Nicht-Typ Parameter .
2.3.4 Spezialisierung . . . . .
2.3.5 Partielle Spezialisierung
2.3.6 Standard Argumente . .
2.3.7 Turing-Vollständigkeit .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
5
5
5
6
6
6
6
7
8
8
9
10
11
12
13
14
18
18
18
19
20
21
23
23
24
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
3 Präprozessor Metaprogrammierung
3.1 Iterationskonzepte . . . . . . . . . .
3.1.1 Primitive horizontale Iteration .
3.1.2 Horizontale Iteration . . . . . . .
3.1.3 Lokale Iteration . . . . . . . . .
3.1.4 Datei Iteration . . . . . . . . . .
3.1.5 Selbst Iteration . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
. 26
. 26
. 26
. 28
. 30
. 32
. 34
4 Template Metaprogrammierung . . . . . . . . . . . . . . . . . . . . . . . 36
4.1 Basistechniken . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36
Sebastian Otte, Fachhochschule Wiesbaden
Seite 3
C++ Metaprogrammierung
Inhaltsverzeichnis
4.1.1 Funktionen . . . . . . . . . . . . . . . . . .
4.1.2 Enumeration vs. konstante Klassenvariable
4.1.3 Type Functions . . . . . . . . . . . . . . . .
4.1.4 Rekursionen . . . . . . . . . . . . . . . . .
4.1.5 Rekusionen und bedingte Verzweigung . . .
4.2 Fortgeschrittene Konzepte . . . . . . . . . . . .
4.2.1 Unrolled Loops . . . . . . . . . . . . . . . .
4.2.2 Expression Templates . . . . . . . . . . . .
5 Anmerkungen und Ergänzungen . . .
5.1 Kritik . . . . . . . . . . . . . . . . . .
5.2 Boost Bibliothek . . . . . . . . . . . .
5.2.1 BPL - Boost Preprocessor Library
5.2.2 MPL - Meta Programming Library
Index
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
36
37
38
39
41
45
45
48
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
. 52
. 52
. 53
. 53
. 53
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54
Literatur
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57
Sebastian Otte, Fachhochschule Wiesbaden
Seite 4
C++ Metaprogrammierung
1 Einführung
1 Einführung
In den folgenden Kapiteln wird das Thema C++ Metaprogrammierung weiträumig
erläutert. Dazu werden zunächst ein paar notwendige Begrifflichkeiten festgelegt. Im
Grundlagenkapitel werden einige Sprachelemente aufgeführt, die für die C++ Metaprogrammierung von Bedeutung sind, bevor anschließend die eigentliche Thematik behandelt wird. Im letzten Kapitel werden Vor- und Nachteile der bis dahin behandelten
Konzepte genannt.
1.1 Der Begriff Metaprogrammierung
Die Vorsilbe meta (griech. µτα - über, nach, hinter) bezeichnet im Allgemeinen eine
von einer grundlegenden Ebene abstrahierten Position. In einem bestimmten Kontext
hat die sogenannte Metaebene eine beschreibende Funktion bezogen auf die abstrahierte
Ebene. So ist Metasprache beispielsweise Sprache über Sprache, oder Metakommunikation meint die Kommunikation über Kommunikation.
Analog verhält es sich mit dem Begriff der Metaprogrammierung, also der Programmierung von Programmierung. Konkret bezeichnet Metaprogrammierung die Generierung
oder Manipulation von Programmcode durch Programmcode.
1.2 Metaprogrammierung mit C++
Allgemein umfasst Metaprogrammierung in C++ Techniken und Konzepte zur Erzeugung von C++ Code durch Sprachelemente von C++. Bei reinem C++ betrifft dies
vor allem die Möglichkeiten durch C++ Templates Algorithmen und Datenstrukturen
zur Übersetzungszeit zu erzeugen. Dabei liegt hier ein besonderer Anspruch auf Verarbeitung zur Übersetzungszeit. Die Template Metaprogrammierung wird beschrieben im
Abschnitt 4.
Aber auch die Metaprogrammierung mit dem Präprozessor soll hier genannt werden.
Präprozessor- Direktiven stammen zwar aus der Sprache C, sind jedoch auch Sprachelement von C++. Die Präprozessor Metaprogrammierung konzentriert sich hauptsächlich
auf die gezielte Wiederholung oder Manipulation von bereits vorhandenem Code. Dieser
Teilbereich der Metaprogrammierung wird im Abschnitt 3 behandelt.
Sebastian Otte, Fachhochschule Wiesbaden
Seite 5
C++ Metaprogrammierung
2 Grundlagen
2 Grundlagen
Um in C++ fortgeschrittene Metaprogrammierung zu betreiben, bedarf es vieler unterschiedlicher Techniken und Kniffe, die bei der „normalen“ Programmierung selten zum
Tragen kommen. Das Grundlagenkapitel soll die Möglichkeit geben, bekanntes Wissen
aufzufrischen und helfen, viele der in späteren Kapiteln verwendeten Techniken umfassend zu verstehen.
2.1 Ein Wort zu Klassen
Strukturen wie struct oder class sind insbesondere in der Template Metaprogrammierung ein wichtiges Basiselement für die meisten Konzepte. Daher ist es durchaus
sinnvoll, das eine oder andere spezifische Detail von Strukturen (Klassen) im Vorfeld zu
durchleuchten.
2.1.1 Der Begriff Klass
Nach [Mül97] sind Strukturen vom Typ struct, class und union quasi gleich. Alle
drei Varianten können Methoden enthalten und erben, bzw. vererben. Ein Unterschied
besteht lediglich in der spezifischen Standardsichtbarkeit (private bei class, public bei
struct und union), und der speziellen gemeinsamen Speicheranordnung von Elementen
in einer union. Im weiteren Verlauf dieser Arbeit wird daher der Begriff Klasse, wenn
nicht anders angegeben, als allgemeine Bezeichnung für solche Strukturen verwendet.
2.1.2 Anonyme Klassen
In C++ lassen sich Strukturen (struct, union und class) auch als „anonym“ deklarieren. Diese Technik wird verwendet um zu verhindern, dass bestimmte Strukturen beliebig instanziert werden können, sondern ausschliesslich bei der Deklaration der Struktur
(siehe [Mül97] S. 69 ff, für mehr Informationen zum Thema anonyme Klassen).
struct
{
int a ;
int b ;
int c ;
} triple ;
Die Variable triple ist, bezogen auf ihren Typ, im gesamten Programm absolut einmalig. Es kann keine weitere Instanz genau dieses Datentyps erzeugt werden.
Sebastian Otte, Fachhochschule Wiesbaden
Seite 6
C++ Metaprogrammierung
2 Grundlagen
struct
{
int a ;
int b ;
int c ;
} triple2 ;
int main ()
{
triple = triple ;
triple = triple2 ;
}
// OK , obgleich sinnlos !
// FEHLER !
Die Deklaration von triple2 ist quasi äquivalent zu der von triple. Jedoch ist es trotzdem nicht möglich, triple triple2 zuzuweisen, da für den Compiler beide Variablen
unterschiedlichen Typs sind.
2.1.3 Innere anonyme Klassen
Eine innere anonyme Klasse bezeichnet eine anynome Klasse, die sich innerhalb einer
benannten Klasse befindet. Anonyme Klassen können auch beliebig tief verschachtelt
werden. Zu beachten ist dabei, dass Elemente einer inneren anonymen Klasse quasi über
den Namensraum der zuletzt aufgetretetenen benannten Klasse innerhalb der Verschachtelungshierarchie verfügbar sind. Dies trifft natürlich nur bei entsprechender Sichtbarkeit
zu.
Das folgende Beispiel zeigt exemplarisch die Verwendung von inneren anonymen Klassen:
struct named
{
struct
{
int a ;
union
{
float x ;
int
x2 ;
};
};
};
int main ()
{
struct named v ;
Sebastian Otte, Fachhochschule Wiesbaden
Seite 7
C++ Metaprogrammierung
v.a
v.x
v . x2
v.z
=
=
=
=
2 Grundlagen
10;
1.0 f ;
20;
100;
}
Der obige Code beinhaltet die Verschachtelung von mehreren inneren anonymen Klassen
(struct und union) innerhalb der benannten Klasse named. Auf alle Elemente innerhalb
der Verschachtelungsstruktur, kann nun über den Namensraum von named zugegriffen
werden und eben diese Verschachtelung erscheint für den Programmierer unsichtbar.
2.2 C Makros
Mit der #define-Direktive lassen sich in C u.a. auch Makros definieren. Makros fungieren als Platzhalter oder Symbole, die man, einmal definiert, überall im Quellcode
verwenden kann. Wenn nun der Präprozessor den Quellcode verarbeitet, wird er alle
darin verwendeten Makro-Symbole durch den Inhalt der entsprechenden Definition ersetzen.
In reinem C werden Makros vor allem für Konstanten verwendet (siehe 2.2.1). Auch
„intelligentere“ parametrisierte Makros (siehe 2.2.3) finden häufig Anwendung. Etwas
seltener verwendet werden komplexe verschachtelte Makros (siehe 2.2.7), jedoch sind
gerade diese für die C bzw. C++-Metaprogrammierung von besonderer Bedeutung.
2.2.1 Einfache Makros
Ein einfaches Makro oder auch #define-Konstante besteht grundsätzlich aus einem Symbol und einem beliebigen Inhalt. Als Inhalt versteht der Präprozessor alles, was sich
zwischen dem Symbol bis zum Ende der Zeile befindet. Wie ein solches Makro aussieht,
zeigt der folgende Code (vgl. [Wol06] S. 118):
# include < stdio .h >
# define NUM
42
# define STRING " ein String \ n "
int main ()
{
int i ;
for ( i = 0; i < NUM ; i ++) // -> for ( i = 0; i < 42;
i ++)
Sebastian Otte, Fachhochschule Wiesbaden
Seite 8
C++ Metaprogrammierung
2 Grundlagen
{
printf ( STRING ) ;
") ;
// -> printf (" ein String \ n
}
return 0;
}
Durch den Präprozessor werden NUM und STRING durch ihren entsprechenden Inhalt im
Code ersetzt.
2.2.2 Mehrzeilige Makros
In C können Makros auch über mehrere Zeilen definiert werden. Das hat den Vorteil,
dass Makro-Definitionen, gerade wenn sie etwas komplexer sind, viel übersichtlicher sein
können. Eine solche Definition geschieht mit Hilfe des \-Zeichens. Wird \ an das Zeilenende einer Makro-Definition gestellt, gehört die nächste Quellcode-Zeile automatisch
mit zur Definition.
Das folgende Listing zeigt eine mehrzeilige Makro-Definition:
# include < stdio .h >
# define MULTI printf ( " zeile1 " ) ; \
printf ( " zeile2 " ) ; \
printf ( " zeile3 " ) ;
int main ()
{
MULTI
return 0;
}
Achtung: Obwohl das obige Makro über drei Zeilen definiert wurde, wird es durch den
Präprozessor trotzdem als nur eine Zeile gelesen. Das liegt daran, dass der Präprozessor
die Zeilenumbruchzeichen, die jeweils hinter dem \ stehen, nicht mit in die Definition
einbezieht. Dieser Umstand entpupt sich später noch als Problem, welches in Kapitel
3 ausführlicher behandelt und gelöst wird. Hier nun die Ausgabe des Präprozessors für
das MULT-Makro:
int main ()
{
printf ( " zeile1 " ) ; printf ( " zeile2 " ) ; printf ( " zeile3 " )
;
return 0;
}
Sebastian Otte, Fachhochschule Wiesbaden
Seite 9
C++ Metaprogrammierung
2 Grundlagen
2.2.3 Parametersubstitution
Neben der einfachen Text- oder Symbolsubstitution bietet der C-Präprozessor ein Konstrukt, um starre Makros „variabel“ zu gestalten. Durch die Parametersubstitution können einem C-Makro bei der Verwendung, ähnlich wie bei einem Funktionsaufruf, Parameter übergeben werden (vgl. [Wol06] S. 119ff). Das nächste Listing zeigt eine einfache
Makrodefinition, die durch Verwendung eines Paramters erweitert wird.
# define SQUARE_TWO 2
# define SQUARE ( x ) ( x * x )
int main ()
{
int a = SQUARE_TWO ;
int b = SQUARE (10) ;
int c = SQUARE ( das ist totaler kaese ) ;
return 0;
}
Im Gegensatz zu einem Funktionsaufruf werden jedoch keine Werte, Variablen etc. übergeben. Der Parameter wird ohne Typüberprüfung oder sonstige semantische Analysen
blind1 substituiert. Dies wird im Folgenden deutlich:
int main ()
{
int a = 2;
int b = (10 * 10) ;
int c = ( das ist totaler kaese * das ist totaler
kaese ) ;
return 0;
}
Der Ausdruck (das ist totaler kaese * das ist totaler kaese) ist natürlich wirklich totaler Käse, da er weder semantisch noch syntaktisch korrekt ist, dennoch wurde
er anstandslos vom Präprozessor generiert.
C-Makros können auch mit mehreren Parametern definiert werden, wie das folgende
Listung zeigt.
# define F_LINEAR (x , a , b )
((( a ) *( x ) ) +( b ) )
1
\
Es gibt lediglich minimale syntaktische Anforderungen an Makro-Parameter. Beispielsweise darf kein
»,« direkt verwendet werden, da es dazu dient, verschiedene Makro-Parameter syntaktisch voneinander zu trennen.
Sebastian Otte, Fachhochschule Wiesbaden
Seite 10
C++ Metaprogrammierung
2 Grundlagen
// Der Klassiker
# define MAX (a , b )
\
(( a ) >=( b ) ?( a ) :( b ) )
int main ()
{
int x = 10;
int l = F_LINEAR (x , 8 , 3) ; // -> (((8) *( x ) ) +(3) )
int m = MAX (10 , 20) ;
// -> ((10) >=(20) ?(10)
:(20) )
return 0;
}
Klammern von Parametern
Grundsätzlich sollten Parameter bei der Verwendung innerhalb von Makros in Klammern
»( ... )« gesetzt werden (vgl. [Wol06] S. 119). Der Grund liegt in der oben genannten
Tatsache, dass ein an das Makro übergebener Ausdruck komplett substituiert wird (bei
einem Funktionsaufruf wird der Ausdrück vor der Übergabe ausgewertet). Man benutzt
also die Klammern, um einen Ausdruck später für den Compiler als zusammenhängend
zu markieren. Zur Veranschaulichung dient hier das Beispiel aus [Wol06]:
# define SQRE1 ( x ) (( x ) * ( x ) )
# define SQRE2 ( x ) (( x * x )
int main ()
{
int a = SQRE1 (5 + 1) ; // -> ((5 + 1) * (5 + 1) )
int b = SQRE2 (5 + 1) ; // -> (5 + 1 * 5 + 1)
}
Klammert man die Parameter wie in diesem Beispiel nicht, kann es passieren, dass für
den Compiler nicht mehr ersichtlich ist, dass es sich ursprünglich um zwei Ausdrücke
gehandelt hat. So erhält a den wert 36 und b den Wert 11.
2.2.4 Variadic Macros
Seit dem C99-Standard dürfen Makros eine variable Anzahl an Parametern haben (vgl.
[C9903] S. 101). Hierbei muss »...« als letzter Parameter in der Makro-Definition angegeben werden. Das nächste Listing soll diese Funktionalität auszugsweise vorstellen.
Sebastian Otte, Fachhochschule Wiesbaden
Seite 11
C++ Metaprogrammierung
# define ERROR (...)
printf ( " file : %s , line : %d , error : " ,
__FILE__ , __LINE__ , __VA_ARGS__ )
2 Grundlagen
\
\
# define SHOW_ME (...) __VA_ARGS__
int main ()
{
ERROR ( " Das ist eine Fehlermeldung mit Nr % d " , 100) ;
SHOW_ME (a , b , c ) ;
return 0;
}
Mit Hilfe der Makro-Definition ERROR lässt sich die Parameterliste »...« durch das vordefinierte Makro __VA_ARGS__ an die printf-Funktion weitergeben. Dabei werden alle
an das Makro übergebene Parameter durch __VA_ARGS__ substituiert. Über das Hilfsmakro SHOW_ME lässt sich die Substitution der Parameterliste leicht nachvollziehen. Nach
Aufruf des Präprozessors ergibt sich folgender Code:
int main ()
{
printf ( " file : %s , line : %d , error : " , " variadic . cpp "
, 9 , " Das ist eine Fehlermeldung mit Nr % d " , 100)
;
a, b, c;
return 0;
}
Betrachtet man das Ergebnis des SHOW_ME-Makros, wird deutlich, wie der Präprozessor
die übergebenen Argumente substituiert hat.
Mit dieser Technik ist es möglich, Funktionen wie printf, die eine variable Parameterliste erwarten, in Makros zu verwenden (wie es das Makro ERROR tut) ohne auf den
Aspekt der variablen Parameterzahl verzichten zu müssen. Das Thema „Parameterlisten
in Funktionen“ wird ausführlich behandelt in [Lou04] Seite 581 ff. Die entsprechende
Definition ist zu finden in [C9903] Seite 85.
2.2.5 Der #-Operator
Ein interessantes Feature von C-Makros ist der eher unbekannte #-Operator. Dieser
Operator, der nur innerhalb eines Makros verwendet werden darf, wandelt ein Makroparameter in eine Zeichenkette um, man spricht auch vom „stringizing“ ([C9903] S. 103).
Das nachfolgende Listing zeigt eine mögliche Verwendung des #-Operators.
Sebastian Otte, Fachhochschule Wiesbaden
Seite 12
C++ Metaprogrammierung
2 Grundlagen
# include < stdio .h >
# define TOSTRING ( a ) # a
# define KUCHEN
# kekse
int main ()
{
printf ( TOSTRING ( das soll ein string werden ) ) ;
printf ( KUCHEN ) ;
return 0;
}
Der Operator hält, was er verspricht und wandelt im TOSTRING-Makro den gesamten
Parameter in einen String um.
int main ()
{
printf ( " das soll ein string werden " ) ;
printf (# kekse ) ;
return 0;
}
Jedoch ist es, wie das KUCHEN-Makro zeigt, nicht möglich, den #-Operator direkt auf Token1 anzuwenden. Dieser Umstand macht aber durchaus Sinn, denn ein Token innerhalb
eines Makros kann auch ebensogut direkt als Zeichenkette formuliert werden.
2.2.6 Der ##-Operator
Eine weiteres Konstrukt, das nur der Verwendung innerhalb von Makros vorbehalten
bleibt, ist der ##-Operator. Dieser binäre Operator dient zur Konkatenation2 von zwei
Token zu einem einzigen. Beispielweise lassen sich durch diesen Operator zwei Makroparameter, die jeweils aus einem Wort bestehen, zu einem Wort zusammenfügen. Am
folgenden Code soll dies exemplarisch illustriert werden.
# include < stdio .h >
# define CONCATENATE (a , b ) a ## b
# define TEST int x ## 1 = 100
int main ()
{
1
Ein Token ist eine Folge von einem oder mehreren Zeichen, die als kleinste, sinngebene Einheit
zusammen zu fassen sind.
2
Konkatenation steht für Aneinanderreihung oder Verkettung.
Sebastian Otte, Fachhochschule Wiesbaden
Seite 13
C++ Metaprogrammierung
2 Grundlagen
CONCATENATE ( int a , 1 = 10) ;
CONCATENATE ( int a , 2 = 20) ;
TEST ;
return 0;
}
Das Makro CONCATENATE soll durch den ##-Operator die beiden Makroparameter a und b
miteinander „vernähen“. Die Resultate der Makroaufrufe finden sich im nächsten Listing.
int main ()
{
int a1
int a2
int x1
return
}
= 10;
= 20;
= 100;
0;
Die Zeilen 3 und 4 zeigen, wie die Makroargumente ohne Whitespaces verbunden wurden.
Es sei anzumerken, dass eine Konkatenation von Makroparametern wirklich nur durch
den ##-Operator möglich ist. Würde man beispielsweise versuchen, im CONCATENATEMakro die Parameter a und b direkt zu verbinden, also ab statt a ## b, könnte der
Präprozessor die Parameter nicht mehr identifizieren, da ab als ein Token interpretiert
wird.
Im Gegensatz zum #-Operator werden beim ##-Operator auch konstante Token1 berücksichtigt. Dies verdeutlicht die Auflösung des TEST-Makros im Vergleich zur Auflösung
des KUCHEN-Makros im vorherigen Abschnitt.
2.2.7 Verschachtelte Makros
Wirklich interessant werden C-Makros durch die Tatsache, dass sie sich beliebig verschachteln lassen. Bereits definierte Makros lassen sich also voll von nachfolgenden2 Makros verwenden. Dazu gehört auch, dass Parameter eines Makros an Untermakros weitergereicht werden können (transitive Parametersubstitution). Das nächste, etwas komplexere Beispiel zeigt eine Anwendungsmöglichkeit verschachtelter Makros. In den folgenden
Listings werden schrittweise einige Werkzeuge (Toolmakros) definiert, die letztendlich
dazu dienen sollen, einen ARGB-Wert (32 bit integer) aus seinen vier Kanälen (Rot,
Grün, Blau und ein Alpha-Kanal mit je 8 bit) zu erzeugen.
Es sei anzumerken, dass es sich hierbei nicht um eine ernst zunehmende Makro-Bibliothek
zur Grafikprogrammierung handelt, sondern um eine exemplarische Verwendung von
verschachtelten Makros.
1
2
Token die keine Parameter sind.
Nachfolgend im Sinne der sequenziellen Parse-Richtung (von oben nach unten).
Sebastian Otte, Fachhochschule Wiesbaden
Seite 14
C++ Metaprogrammierung
2 Grundlagen
# include < stdio .h >
# define BSR ( value , shift ) \
( value >> shift )
# define BSL ( value , shift ) \
( value << shift )
Zunächst wurden zwei binäre Makros BSR und BSL definiert für einen variablen „Bitshift“
nach recht und links.
# define BSR8 ( value ) BSR ( value , 8)
# define BSR16 ( value ) BSR ( value , 16)
# define BSR24 ( value ) BSR ( value , 24)
# define BSL8 ( value ) BSL ( value , 8)
# define BSL16 ( value ) BSL ( value , 16)
# define BSL24 ( value ) BSL ( value , 24)
Basierend auf den allgemeinen Bitshift-Makros, wurden diese durch unäre Makros für
die tatsächlich benötigten Sprünge spezialisiert. Benötigt werden dabei Sprünge um 8
bit, 16 bit und 24 bit. Diese genügen, um ein Byte bzw. einen Farbkanal beliebig auf
alle anderen Farbkanäle zu verschieben. Es werden im Anschluss nur noch die Spezialisierungen verwendet.
# define AND ( first , second ) \
( first & second )
# define AND_FF ( value ) AND ( value , 0 xFF )
# define OR ( first , second ) \
( first | second )
Weitere notwendige Werkzeuge sind das AND-Makro, insbesondere die spezialisierte Version AND_FF zur Isolierung eines Bytes und das OR-Makro, um die Farbkanäle zu kombinieren.
# define ARGB_2_ALPHA ( alpha ) \
( BSL24 ( AND_FF ( alpha ) ) )
Sebastian Otte, Fachhochschule Wiesbaden
Seite 15
C++ Metaprogrammierung
2 Grundlagen
# define ARGB_2_RED ( red ) \
( BSL16 ( AND_FF ( red ) ) )
# define ARGB_2_GREEN ( green ) \
( BSL8 ( AND_FF ( green ) ) )
# define ARGB_2_BLUE ( blue ) \
( AND_FF ( blue ) ) )
Nachdem die vorherigen Makros ziemlich allgemein definiert sind (und man sie dadurch
leicht wiederverwenden kann), wird es nun etwas konkreter. Unter Verwendung der bereits erstellten Werkzeuge erzeugen die vorliegenden Makros Code, um ein Byte „gesäubert“ in den entsprechenden Farbkanal zu transportieren.
# define ARGB ( alpha , red , green , blue )
\
( OR ( ARGB_2_ALPHA ( alpha ) , ( OR ( ARGB_2_RED ( red ) ,
\
( OR ( ARGB_2_GREEN ( green ) , ARGB_2_BLUE ( blue ) ) ) ) ) ) )
An oberster Stelle fügt schließlich das ARGB-Makros alles zusammen. Es verbindet die vier
Farbkanal-Makros mit dreifacher, versetzt geschachtelter Verwendung des OR-Makros.
Die folgende Abbildung soll die Struktur der Integration verdeutlichen.
ARGB
OR
ARGB_2_ALPHA
...
OR
ARGB_2_RED
OR
... ARGB_2_GREEN
...
ARGB_2_BLUE
...
Die obige Baumstruktur zeigt wie die Makros von „unten nach oben“ substituiert werden. Der Vollständigkeit halber müssten noch die AND- und Bitshift-Makros in den Baum
eingehängt werden, jedoch wurde zu Gunsten der Übersicht darauf verzichtet.
Nach dem theoretischen Aufbau soll das ARGB-Makro im folgenden Listing einmal angewendet werden.
Sebastian Otte, Fachhochschule Wiesbaden
Seite 16
C++ Metaprogrammierung
int main ()
{
unsigned
unsigned
unsigned
unsigned
unsigned
int
int
int
int
int
2 Grundlagen
argb = 0;
a = 0 xAA ;
r = 0 xBB ;
g = 0 xCC ;
b = 0 xDD ;
argb = ARGB (a , r , g , b ) ;
printf ( argb ) ;
return 0;
}
Das Makro wird mit den vier Variablen a, r, g, b parametrisiert und wird diese, nicht
deren Inhalt, substituieren. Was nun schlußendlich der Präprozessor auf dem Ausdruck
macht, zeigt das nachfolgende Listing.
int main ()
{
unsigned int argb = 0;
unsigned
unsigned
unsigned
unsigned
int
int
int
int
a
r
g
b
=
=
=
=
0 xAA ;
0 xBB ;
0 xCC ;
0 xDD ;
argb = ((((( a & 0 xFF ) << 24) ) | ((((( r & 0 xFF ) <<
16) ) | ((((( g & 0 xFF ) << 8) ) | (( b & 0 xFF ) ) ) ) ) ) ) )
);
printf ( argb ) ;
return 0;
}
Das ARGB-Makro wurde in einen ziemlich komplexen Ausdruck aufgelöst, der die funktionale Anforderung, nämlich das Erzeugen eines ARGB-Farbwertes aus den vier Einzelkomponenten erfüllt.
Sebastian Otte, Fachhochschule Wiesbaden
Seite 17
C++ Metaprogrammierung
2 Grundlagen
2.2.8 Turing-Vollständigkeit
Einen Beweis, dass C-Makros nicht Turing-vollständig sein können, ist informell leicht
zu erbringen. Es gibt mit reinen C-Makros, keinen Weg beliebige tiefe Rekursionen (siehe Iterationskonzepte im Abschnitt 3.1) zu formulieren. Es ist nicht mal möglich, dass
ein Makro sich selber aufruft. Das liegt schlicht und ergreifend daran, dass der Präprozessor nur einmal von oben nach unten über den Quellcode läuft und ein Makro erst
dann bekannt ist, wenn es komplett definiert wurde. Für eine geeignete Rekursion müsste der Präprozessor einen Quellcodebereich mehrmals durchlaufen können. Desweiteren
fehlt die Möglichkeit, innerhalb von Makros bedingte Entscheidungen zu treffen, wie es
außerhalb bespielsweise mit #if möglich ist.
2.3 C++ Templates
C++ Templates sind, wie bereits erwähnt, ein Konstrukt, um möglichst allgemeinen
Quellcode zu schreiben. C++ hat den Vorteil bzw. Nachteil, stark typgebunden zu sein.
Ein einmal konkret formulierter Algorithmus ist an einen bestimmten Datentypen gebunden und lässt sich in der Regel nicht für andere verwenden. Mit Templates erstellt
man nun quasi Schablonen für Algorithmen und Datenstrukturen. Beispielsweise lässt
sich eine verkettete Liste mit Hilfe von Templates so allgemein implementieren, dass sie
für eine ganze Reihe von Datentypen verwendbar wird.
Die Template-Programmierung ist mit all ihren Facetten ein extrem umfangreiches Thema und wird in diesem Kapitel nur auszugsweise vorgestellt. Eine umfassende Auseinandersetzung mit C++ Templates und ausführliche Erläuterungen liefert [VJ07].
2.3.1 Funktions-Templates
Funktions-Templates dienen dazu, die besagte Möglichkeit der Verallgemeinerung direkt
auf Funktionen anzuwenden. So können beliebige Parameter einer Funktion inklusive
der Funktions-Rückgabe durch einen entsprechenden generischen Datentyp ausgetauscht
werden. Das folgende Listing zeigt die Implementierung der bekannten Max-Funktion
mit Hilfe eines Funktions-Templates.
# include < iostream >
# include < iomanip >
template < typename T >
T max ( const T & first , const T & second )
{
return ( first >= second ) ?( first ) :( second ) ;
}
Sebastian Otte, Fachhochschule Wiesbaden
Seite 18
C++ Metaprogrammierung
2 Grundlagen
int main ()
{
std :: cout << max (10 , 20) << std :: endl ;
std :: cout << max (1.0 f , 1.1 f ) << std :: endl ;
}
Durch das Schlüsselwort template wird die Template-Definition initialisiert. Innerhalb
der spitzen Klammern findet sich die eigentliche Definition. Im obigen Beispiel besteht
das Template nur aus einem Typ-Parameter T, der durch das Schlüsselwort typename1
bezeichnet wurde. Durch das Funktions-Template kann nun die einmal implementierte
Funktion max mit unterschiedlichen Typen arbeiten, wie es in Zeile 12 und 13 der Fall
ist. Der Compiler erkennt mit welchem Typ das Funktions-Template gerufen wurde und
erzeugt bei Bedarf aus der Vorlage die Funktion für diesen. Das quasi automatische Erkennen des übergebenen Datentyps nennt man (implizite) Typ-Deduktion. Nicht immer
gelingt eine solche implizite Typisierung. Manchmal ist es notwendig, dem Compiler explizit mitzuteilen, für welchen Typ die Funktion erzeugt werden soll. Das geht, in dem
man der Funktion den gewünschten Typen in spitzen Klammern beim Funktionaufruf
mitgibt. Der Aufruf max<int>(1, 2) erzeugt ohne Rücksicht auf die Parameter eine
int-Instanz der max-Funktion.
2.3.2 Klassen-Templates
Templates lassen sich aber auch für Klassen definieren (vgl. [Wol06] S. 485). Der Begriff
Klasse schließt, wie bereits erwähnt, structs und unions ein.
template < typename T >
class Box
{
public :
typedef T var_type ;
T var ;
};
int main ()
{
Box < int >:: var_type x = 10;
Box < int > b ;
b . var = x ;
1
Oft wird auch das Schlüsselwort class verwendet, welches an dieser Stelle exakt die gleiche Bedeutung hat. Man sagt jedoch, class statt typename zu verwenden sei veraltet und nur aus
Kompatibilitätsgründen erlaubt (vgl. [VJ07]).
Sebastian Otte, Fachhochschule Wiesbaden
Seite 19
C++ Metaprogrammierung
2 Grundlagen
return 0;
}
Das Beispiel zeigt ein Klassen-Template für die Klasse Box. Das Template wird explizit
mit einem int typisiert. Dabei erzeugt der Compiler eine Klasse nach dem obigen Bauplan und ersetzt jedes Vorkommen von T durch int. Durch die Typisierung wird jedoch
noch keine Klasseninstanz erzeugt, sondern nur eine konkrete, typgebundene Klasse aus
einem allgemeinen Klassen-Template, das der dann eine Objektinstanz erzeugt wird.
Interessant ist, dass der übergebene Datentyp durch typedef als Typ-Eigenschaft der
Klasse zugänglich gemacht und beispielsweise ausserhalb der Klasse benutzt werden
kann. Das ermöglicht im Vergleich zu herkömmlichem C/C++ eine besondere Technik
(siehe dazu 4.1.3).
2.3.3 Nicht-Typ Parameter
Die bisher vorgestellten Templates wurden nur durch Datentypen parametrisiert. C++
Templates können jedoch auch mit Nicht-Typ Parameter verwendet werden (vgl. [VJ07]
S. 39). Dadurch kann man die Codegenerierung eines Templates durch konstante Werte
beeinflussen. Beispielweise könnte man einen generischen Stack-Container, der durch
ein statisches Array implementiert ist, in seiner Größe durch einen konstanten Wert
zur Übersetzungszeit festlegen. Dadurch entscheidet der Programmierer, der den Stack
verwendet, wie groß dieser ist, nicht der Programmierer des Stacks.
Als Nicht-Typ Parameter können grundsätzlich alle diskreten Standard-Datentypen1
eingesetzt werden. Das nächste Listing zeigt eine einfache Anwendung von Nicht-Typ
Parametern in Templates.
# include < iostream >
template < typename T , int N >
struct array
{
static const int value = N ;
enum { value_enum = N };
T data [ N ];
};
int main ()
{
array < int , 1024 > a ;
std :: cout << sizeof ( a . data ) << std :: endl ;
1
Dazu gehören nicht Fließkomma-Datentypen, Pointer und Klassen-Typen.
Sebastian Otte, Fachhochschule Wiesbaden
Seite 20
C++ Metaprogrammierung
2 Grundlagen
std :: cout << array < int , 1024 >:: value
<< std ::
endl ;
std :: cout << array < int , 1024 >:: value_enum << std ::
endl ;
return 0;
}
Das im obigen Beispiel präsentierte array-Template beinhaltet ein Array vom Datentyp
T mit der Größe von N. Durch Eingabe einer konstanten int-Zahl bei der Instanzierung
des Templates wird ein Array mit genau dieser Anzahl an Elementen erzeugt. Wenn es
aus irgendeinem Grund notwendig ist, ohne Instanzierung nochmal auf die übergebene
Zahl zuzugreifen, gibt es direkt genau zwei Möglichkeiten :
• der Wert N wird in einer konstanten Klassenvariable gespeichert
• in einer anonymen Aufzählung (Enumeration) wird N als einziges Element aufgenommen
In beiden Fällen kann über den statischen ::-Operator auf die Werte zugegriffen werden. Die Möglichkeit von außerhalb eines Templates statisch auf Nicht-Typ Parameter
zugreifen zu können, ist in der Template Metaprogrammierung ein mächtiges Werkzeug
(siehe hierzu Abschnitt 4.1.1).
2.3.4 Spezialisierung
Manchmal entstehen durch die Verwendung von Templates auch Probleme, die aus verallgemeinerten Algorithmen resultieren. Man stelle sich einmal einen generischen Container vor, der seine Elemente in irgendeiner Weise sortieren muss. Spätestens wenn
der Container String-Elemente1 enthält, wird deutlich, dass die Sortierung anders vonstatten gehen muss als beispielsweise bei Ganzzahlen. Gerade jetzt wäre es hilfreich,
den Container für bestimmte Datentypen anpassen zu können. Durch Spezialierungen
von Templates ist genau dieses möglich (vgl. [VJ07] S. 27ff). Die einfachste Variante
Templates zu spezialisieren zeigt das nachfolgende Listing.
# include < iostream >
template < typename T >
class Box2
{
public :
1
ein String ist in der Regel als
char* oder const char* implementiert.
Sebastian Otte, Fachhochschule Wiesbaden
Seite 21
C++ Metaprogrammierung
2 Grundlagen
typedef T var_type ;
T value ;
std :: string getTypename () { return " unbekannt " ;
}
};
template <>
class Box2 < int >
{
public :
typedef T var_type ;
int value ;
std :: string getTypename () { return " integer " ; }
};
int main ()
{
Box2 < int >
a;
Box2 < double > b ;
std :: cout << a . getTypename () << std :: endl ;
std :: cout << b . getTypename () << std :: endl ;
return 0;
}
Das aus einem vorherigen Beispiel bekannte Box-Template wird hier erweitert. Hinzufügt
wurde eine Methode, die den Namen des gekapselten Typens zurückgeben soll. Zunächst
ist das allgemeine Template definiert, nach dem getTypename »unbekannt« liefert. Durch
eine Spezialisierung für den Typen int ab Zeile 13 liefert getTypname in diesem Fall
»integer«. Diese Form der Spezialisierung hat jedoch den Nachteil, dass immer ganze
Template-Definitionen ausgetauscht werden. Beispielsweise muss bei der Spezialisierung
eines Klassen-Templates die gesamte Klasse in die Spezialisierung übernommen werden.
Ein sehr eleganter Weg dies zu umgehen, ist eine gezielte Spezialisierung von einzelnen
Methoden. Für das Box2-Template würde eine solche Spezialisierung wie folgt aussehen:
template <>
std :: string Box2 < int >:: getTypename ()
{
return " integer " ;
}
Diese Implementierung hat im Prinzip denselben Effekt wie die Implementierung im
vorherigen Listing, ist jedoch wesentlich kürzer und eleganter.
Sebastian Otte, Fachhochschule Wiesbaden
Seite 22
C++ Metaprogrammierung
2 Grundlagen
2.3.5 Partielle Spezialisierung
Mal angenommen ein Template hat mehrere Parameter, ob Typ oder Nicht-Typ Parameter spielt keine Rolle, dann lässt sich dieses Template auch partiell spezialisieren.
D.h. es werden gezielt einzelne Parameter spezialisiert (vgl. [VJ07] S. 29). Das nächste
Listing zeigt exemplarisch ein paar Möglichkeiten der partiellen Spezialisierung.
template < typename T1 , typename T2 >
struct some
{
// ...
};
template < typename T >
struct some <T , int >
{
// ...
};
template < typename T >
struct some <T , T >
{
// ...
};
Die erste partielle Spezialisierung des Templates some erfasst den Fall, dass es some
mit T2 = int typisiert wurde, T1 bleibt weiterhin variabel. Die zweite teilweise Spezialisierung tritt für den Fall in Kraft, wenn beide Typen, mit denen das Template
parametrisiert wurde, gleich sind.
Manchmal stellt sich die Frage, welche Spezialisierung wohl vom Compiler gewählt wird.
Grundsätzlich binden konkrete Typ-Zuordnungen stärker als „allgemeine“ Übereinstimmungen. Bei spezialisierten Templates kann aber auch für bestimmte Typisierungen
keine eindeutige Zuordnung erfolgen. Im obigen Beispiel führt die Typisierung some<int
, int>... zu einem Fehler, da beide Spezialisierung in Frage kommen und, da beide
einen allgemeinen Anteil haben, ist die Zuordnung »ambiguous«, also nicht eindeutig.
2.3.6 Standard Argumente
Änhlich wie bei Funktionen gibt es für Templates die Möglichkeit, für Parameter Standardargumente festzulegen. Standardargumente treten genau dann in Kraft, wenn bei
der Template-Instanzierung kein Argument für den entsprechenden Parameter angegeben wurde. Im nachfolgenden Code wird exemplarisch ein Standardargument definiert.
Sebastian Otte, Fachhochschule Wiesbaden
Seite 23
C++ Metaprogrammierung
2 Grundlagen
template < typename T = int >
struct data
{
T value ;
};
int main ()
{
data < >
a;
data < float > b ;
// a . value ist ein int
// b . value ist ein float
return 0;
}
Wenn kein Argument an das Template data übergeben wird, wählt der Compiler das
definierte Standardargument int. Wird dem Template explizit ein Typ übergeben, wird
kein Standardargument gewählt.
2.3.7 Turing-Vollständigkeit
Im Gegensatz zur C-Präprozessor Programmierung ist die Template Programmierung
Turing-vollständig. Beweisen lässt sich dies durch die Tatsache, dass sich alle partiell rekursiven Funktionen auch mit Templates programmieren lassen (für Turing-Vollständigkeit,
rekursive Funktionen siehe [EP08]). Überraschend ist dies, wenn man die Aussage auf die
Übersetzungszeit einschränkt. Etwas klarer formuliert bedeutet dies, dass sich theoretisch alle Funktionen, die man überhaupt mit heutzutage bekannten Rechnern berechnen
kann, auch mit C++ Templates und zwar während der Übersetzung des Codes berechnen lassen.
Einen interessanten Beweis für die Turing-Vollständigkeit von C++ Templates lieferte
Todd L. Veldhuizen in [Vel03] dadurch, dass er mit Hilfe von Templates zur Übersetzungszeit eine Turing-Maschine simuliert hat.
Übrigens war die Tatsache, dass Template Programmierung Turing-vollständig ist, den
Entwicklern von Templates nicht bewusst. Es war Erwin Unruh, der 1994 erstmals das
Bewusstsein dafür eröffnete. Während eines Treffens des C++ Standardisierungskomitees, präsentierte er ein Programm, welches durch Templates Primzahlen berechnen
konnte. Zum Beweis, dass dies zur Übersetungszeit geschieht, wurde das Programm
so formuliert, dass der Compiler das Übersetzen mit Fehlermeldungen abbricht. Jedoch
enthalten diese Fehlermeldungen die gewünschten Primzahlen. Der folgende Code zeigt
das Primzahlen Template von Erwin Unruh (stammt aus [VJ07] S. 318 ff).
template < int p , int i >
struct is_prime {
enum { prim = ( p == 2) || ( p % i )
Sebastian Otte, Fachhochschule Wiesbaden
&&
Seite 24
C++ Metaprogrammierung
2 Grundlagen
is_prime <(( i > 2) ? p : 0) , i - 1 >::
prim };
};
template <>
struct is_prime <0 , 0 > { enum { prim = 1 }; };
template <>
struct is_prime <0 , 1 > { enum { prim = 1 }; };
template < int i >
struct D { D ( void *) ; };
template < int i >
class Prime_print {
public :
Prime_print < i - 1 > a ;
enum { prim = is_prime <i , i - 1 >:: prim };
void f () { D <i > d = prim ? 1 : 0; a . f () ; }
};
template <>
class Prime_print <1 > {
public :
enum { prim = 0 };
void f () { D <1 > d = prim ? 1 : 0; }
};
int main ()
{
Prime_print <200 > a ;
a . f () ;
}
Sebastian Otte, Fachhochschule Wiesbaden
Seite 25
C++ Metaprogrammierung
3 Präprozessor Metaprogrammierung
3 Präprozessor Metaprogrammierung
Dass mit dem C Präprozessor mehr möglich, ist als einfache Kompilerweichen oder Makros zu implementieren, sollte bereits durch das Grundlagenkapitel deutlich geworden
sein. In diesem Kapitel werden nun einige Möglichkeiten erläutert, wie mit dem Präprozessor teils komplexe Metaprogrammierung betrieben werden kann, was sich hier jedoch
hauptsächlich auf die gezielte Erzeugung bzw. Wiederholung von Codesegmenten beschränkt.
In der Praxis ist es meist so, dass Präprozessor Metaprogrammierung als Hilfsmittel bei
der Metaprogrammierung mit C++ Templates eingesetzt wird.
3.1 Iterationskonzepte
Hinter dem Begriff Iteration stehen bei der Präprozessor Metaprogrammierung eine ganze Reihe von Techniken, die es ermöglichen, bestimmte Codesequenzen zu expandieren1 .
Durch die starke Beschränktheit des Präprozessors sind einer beliebigen Expansion jedoch klare Grenzen gesetzt. Dennoch oder vielleicht gerade deshalb sind die Möglichkeiten durchaus beeindruckend.
Die Wortschöpfungen und hier aufgeführten Konzepte entstammen größtenteils der BPL2
(vgl. [DAR09]).
3.1.1 Primitive horizontale Iteration
Die primitive horizontale Iteration ist ein sehr unflexibles, dafür aber einfaches Konzept,
um einen bestimmten Code gezielt zu expandieren. Der zu expandierende Code wird
dabei fest in die entsprechenden Makros eingebettet. Die gesamte Expansion resultiert
einzeilig, deshalb der Begriff »horizontale« Iteration. Das folgende Beispiel zeigt, wie
eine primitive horizontale Iteration aussehen kann.
class Ls
{
public :
Ls ( int h , Ls * t ) : head ( h ) , tail ( t ) {}
int head ;
Ls * tail ;
};
1
2
Codeauschnitte werden gezielt wiederholt und varieert.
BPL - Boost Preprocessor Library, siehe Abschnitt 5.2.1.
Sebastian Otte, Fachhochschule Wiesbaden
Seite 26
C++ Metaprogrammierung
3 Präprozessor Metaprogrammierung
Hierbei handelt es sich um eine simple einfach-verkettete Liste für int-Werte. Der
Ausdruck new Ls(1, new Ls(2, new Ls(3, new Ls(4, 0))) würde beispielsweise eine solche Liste mit vier Elementen initialisieren. Es ist natürlich sehr umständlich jedes
Mal, wenn eine solche Listen-Instanz gebraucht wird, einen derartigen Ausdruck zu formulieren. Dieser Sachverhalt lässt sich durch die folgenden Makrodefinitionen vereinfachen.
# define LS (a , b ) new Ls (a , b )
Das Makro LS(a, b) ist das Basismakro dieser Iteration, denn es definiert, was in einem
Iterationsschritt passieren soll.
# define
# define
# define
# define
# define
// ...
LS_1 ( a )
LS_2 (a ,
LS_3 (a ,
LS_4 (a ,
LS_5 (a ,
LS (a , 0)
b ) LS (a , LS_1 ( b ) )
b , c ) LS (a , LS_2 (b , c ) )
b , c , d ) LS (a , LS_3 (b , c , d ) )
b , c , d , e ) LS (a , LS_4 (b , c , d , e ) )
Die Makros LS_1 bis LS_51 bilden nun statisch die eigentliche Iteration. LS_1 ist hier
das Ende der Iteration, während alle Folgemakros ihren direkten Vorgänger als zweites
Argument an das Basismakro übergeben. Der Aufruf eines der LS_x-Makros führt so zu
einer verschachtelten Makrosubstitution bis LS_1 erreicht wird. Nun ist es möglich, den
weiter oben beschriebenen Ausdruck mit Hilfe eines entsprechenden Makroaufrufes zu
ersetzen.
// before preprocessing
int main ()
{
Ls * l = LS_4 (1 , 2 , 3 , 4) ;
return 0;
}
Interessant ist nun, wie der Präprozessor das Makro verarbeitet hat. Er erzeugt hier
nämlich exakt den Ausdruck, der durch Verwendung der Iterations-Makros vermieden
wurde.
// after preprocessing
int main ()
{
Ls * l = new Ls (1 , new Ls (2 , new Ls (3 , new Ls (4 , 0) ) )
);
return 0;
}
1
Die Iterationstiefe kann hier beliebig fortgesetzt werden.
Sebastian Otte, Fachhochschule Wiesbaden
Seite 27
C++ Metaprogrammierung
3 Präprozessor Metaprogrammierung
3.1.2 Horizontale Iteration
Ein Augenmerk bei der Programmierung von Software liegt oft auf einer möglichst hohen
Wiederverwendbarkeit des erarbeiteten Quellcodes. Aus eben diesem Gesichtspunkt ist
das vorherige Konzept ziemlich ungeeignet, da es, wie es in dem Beispiel gegeben wurde,
nur genau einem konkreten Zweck dient. Aber genau das unterscheidet die horizontale Iteration zu seiner primitiven Variante: Eine wesentlich höhere Allgemeingültigkeit.
Aber wie kann das aussehen?
Die Idee ist, jenes Makro, welches bei einem Iterationsschritt eingesetzt wird, an den
Iterationsprozess übergebbar zu machen, so dass ein einmal entwickeltes Iterationskonstrukt für unterschiedliche Zwecke verwendet werden kann.
Im folgenden Beispiel wird ein Konstrukt erzeugt, mit dem es möglich ist, ein gegebenes Makro wie in einer for-Schleife zu expandieren. Der formale Anspruch an das
Makro, das bei jedem Iterationsschritt eingesetzt werden soll, ist es, genau einen Parameter zu haben.
Zunächst ein wichtiges Hilfsmakro:
# define
# define
# define
# define
INC ( i ) INC_ ## i
INC_0 1
INC_1 2
INC_2 3
...
# define INC_17 18
# define INC_18 19
# define INC_19 20
Dieses Makrokonstrukt bedient sich eines für die Präprozessorprogrammierung elementaren Tricks. INC erwartet bei Verwendung ein Argument i. Dabei sollte i eine Ganzahl,
hier zwischen 0 und 19, sein. Wird nun INC beispielsweise mit dem Argument 4 parametrisiert, passiert folgendes: Der Präprozessor substituiert zunächst i in der Definition
von INC mit 4. Durch den ##-Operator wird die substituierte 4 mit dem vorherigen Token INC_ verschmolzen. Das Resultat INC_4 wird anschließend durch den Präprozessor
wieder als Makro interpretiert und INC_4 ist hier als 5 definiert. Der Aufruf INC(x)
ergibt also den Wert x + 1.
Als nächstes wird das Initialmakro FOR der Iteration wie folgt definiert:
# define FOR ( start , loops , macro )
\
FOR_ ## loops ( start , macro )
Der erste Parameter (start) gibt an, bei welchem Wert (positive Ganzzahl) die Iteration
beginnen soll. Als zweiten Parameter (loops) erwartet FOR wieder eine positive Ganz-
Sebastian Otte, Fachhochschule Wiesbaden
Seite 28
C++ Metaprogrammierung
3 Präprozessor Metaprogrammierung
zahl, die angibt, mit wie vielen Durchläufen der dritte Parameter (macro) expandiert
werden soll. Ein Argument für macro muss nicht zwangsläufig ein Makro sein, nur der
Ausdruck macro(i) sollte gültig sein.
Die entsprechenden Iterationsschritte sehen folgendermaßen aus:
# define FOR_20 (i , macro ) macro ( i ) FOR_19 ( INC ( i ) , macro )
# define FOR_19 (i , macro ) macro ( i ) FOR_18 ( INC ( i ) , macro )
# define FOR_18 (i , macro ) macro ( i ) FOR_17 ( INC ( i ) , macro )
...
# define FOR_3 (i , macro ) macro ( i ) FOR_2 ( INC ( i ) , macro )
# define FOR_2 (i , macro ) macro ( i ) FOR_1 ( INC ( i ) , macro )
# define FOR_1 (i , macro ) macro ( i )
Die verschachtelte Makrosubsitution, die hier als Iteration bezeichnet wird, funktioniert,
obwohl sie komplexer anmutet, nach dem gleichen Prinzip wie das primitive Derivat des
vorherigen Abschnitts. Auch hier sind die einzelnen Iterationsschritte wieder statisch
verdrahtet. Es fällt auf, dass eben diese Verdrahtung absteigend definiert ist (FOR_20
benutzt FOR_19, FOR_19 benutzt FOR_18 usw.). Das ist deshalb so, weil die Zahl nach
FOR_ angibt, wie viele Iterationsschritte noch zu durchlaufen sind. Während sich die
Anzahl der zu durchlaufenden Iterationen bei jedem Schritt um 1 verringert, wird der
eigentliche Schleifenzähler bei jedem Schritt um 1 erhöht.
Wichtig ist es noch anzumerken, dass es in dem hier konstruierten Beispiel notwendig
ist, in einem Iterationsschritt erst das übergebene Makro einzusetzen und dann weiter
zu iterieren (prefix order).
Der nachfolgende Codeabschnitt zeigt exemplarisch die Anwendung der horizontalen
Iteration durch das Marko FOR.
// before preprocessing
int main ()
{
int array [20];
#
#
#
#
define MY_MACRO1 ( i ) array [ i ] = i ;
FOR (0 , 20 , MY_MACRO1 )
undef MY_MACRO1
define MY_MACRO2 ( i ) printf ( " % d \ n " , i )
FOR (0 , 10 , MY_MACRO2 )
undef MY_MACRO2
return 0;
Sebastian Otte, Fachhochschule Wiesbaden
Seite 29
C++ Metaprogrammierung
3 Präprozessor Metaprogrammierung
}
Beim ersten Anwendungsfall mit MY_MACRO erzeugt der Präprozessor den Code:
// after preprocessing
// outout of FOR (0 , 20 , MY_MACRO1 )
array [0] = 0; array [1] = 1; array [2] = 2; array [3] = 3;
array [4] = 4; array [5] = 5; array [6] = 6; array [7] = 7;
array [8] = 8; array [9] = 9; array [10] = 10; array [11] =
11; array [12] = 12; array [13] = 13; array [14] = 14;
array [15] = 15; array [16] = 16; array [17] = 17; array
[18] = 18; array [19] = 19;
Im zweiten Anwendungsfall sieht das Resultat der Exansion aus wie folgt:
// after preprocessing
// outout of FOR (0 , 10 , MY_MACRO2 )
printf ( " % d \ n " , 0) printf ( " % d \ n " , 1) printf ( " % d \ n " , 2)
printf ( " % d \ n " , 3) printf ( " % d \ n " , 4) printf ( " % d \ n " , 5)
printf ( " % d \ n " , 6) printf ( " % d \ n " , 7) printf ( " % d \ n " , 8)
printf ( " % d \ n " , 9)
Die horizontale Iteration am Beispiel FOR zeigt, dass man mit relativ wenig Aufwand
bei der Verwendung schnell individuelle Makros expandieren kann. Dem gegenüber steht
natürlich der hohe Aufwand der Entwicklung eines solchen Iterationskonzeptes.
Nach wie vor hat die Expansion einen besonderen Nachteil: Sie resultiert in einer Zeile
Code. Spätestens dann, wenn komplexere Strukturen expandiert werden, gestaltet sich
das Debugging extrem schwierig, da der Compiler im Fehlerfall meistens nur die Zeilennummer nennt und da sich durchaus tausende von Bytes in einer Zeile befinden können,
wird die Information „in welcher Zeile ein Fehler liegt“ schnell irrelevant.
3.1.3 Lokale Iteration
Die lokale Iteration ist ein Konzept, um das Problem der „einzeiligen“ Expansion der
vorherigen Konzepte zu umgehen. Das ist jedoch über den herkömmlichen Weg durch
Parametersubstitution auf Makro-Ebene nicht möglich, da ein Makro keine Zeilenumbrüche enthalten kann. Man muss den Präprozessor also irgendwie zwingen, zwischen
den Iterationsschritten Zeilenumbrüche auszugeben. Die Idee des Verfahrens ist einfach,
wie genial: Es gibt eine Schleifendatei, in der alle möglichen Iterationsschritte bereits
ausgeschrieben vorliegen, wobei jeder Schritt mit Hilfe von #if nur durch Zutreffen der
Bedingung „Iterationsschritt liegt im gegebenen Iterationsintervall“ zu erreichen ist. Innerhalb der Abfrage wird das entsprechende Iterationsmakro lokal (daher der Name des
Verfahrens) eingesetzt.
Sebastian Otte, Fachhochschule Wiesbaden
Seite 30
C++ Metaprogrammierung
3 Präprozessor Metaprogrammierung
Wie eine konkrete Implementierung der lokalen Iteration aussehen kann, zeigt das folgende Beispiel. Nach Konvention sind nachstehende Makros vor der eigentlichen Iteration
zu definieren:
• FOR_LOC_START gibt den Startwert des Schleifenzählers an,
• FOR_LOC_COUNT bestimmt die Anzahl der Iterationen,
• FOR_LOC_MACRO ist das zu expandierende Makro.
Innerhalb der Schleifendatei (hier for_loc.h) wird zunächst die oben beschriebene Iterationsbedingung formuliert:
# define FOR_LOC_COND ( i ) ( FOR_LOC_START ) <= ( i ) && ((
FOR_LOC_START ) +( FOR_LOC_COUNT ) ) > ( i )
Die Bedingung FOR_LOC_COND ist genau dann erfüllt, wenn i innerhalb des Intervals
FOR_LOC_START (inklusiv) und FOR_LOC_START + (FOR_LOC_COUNT) (exklusiv) liegt.
Mit Hilfe von FOR_LOC_COND lässt sich nun jeder Iterationsschritt mit seinem entsprechenden Wert „absichern“:
# if FOR_LOC_COND (0)
FOR_LOC_MACRO (0)
# endif
# if FOR_LOC_COND (1)
FOR_LOC_MACRO (1)
# endif
...
# if FOR_LOC_COND (18)
FOR_LOC_MACRO (18)
# endif
# if FOR_LOC_COND (19)
FOR_LOC_MACRO (19)
# endif
Ein Aufruf der Iteration sieht nun folgendermaßen aus:
// before preprocessing
int main ()
{
int array [20];
#
define FOR_LOC_START 0
Sebastian Otte, Fachhochschule Wiesbaden
Seite 31
C++ Metaprogrammierung
3 Präprozessor Metaprogrammierung
#
#
define FOR_LOC_COUNT 20
define FOR_LOC_MACRO ( i ) printf ( " % dline " , i ) ;
#
include " for_loc . h "
return 0;
}
Vor dem Aufruf der Iteration #include "for_loc.h" werden nach Konvention die
Schleifenparameter festgelegt. Die Ausgabe des Präprozessors sieht aus wie folgt:
// after preprocessing
int main ()
{
int array [20];
printf ( " % dline " , 0) ;
printf ( " % dline " , 1) ;
printf ( " % dline " , 2) ;
...
printf ( " % dline " , 17) ;
printf ( " % dline " , 18) ;
printf ( " % dline " , 19) ;
return 0;
}
3.1.4 Datei Iteration
Die Datei Iteration läßt sich quasi auf die gleiche Weise implementieren, wie die lokale
Iteration mit der Ausnahme, dass kein Makro expandiert wird, sondern stattdessen eine übergebene Datei in jedem Iterationsschritt inkludiert wird. Der Konfiguration der
Iteration geschieht nun durch:
• FOR_FILE_START gibt den Startwert des Schleifenzählers an,
• FOR_FILE_COUNT bestimmt die Anzahl der Iterationen,
• FOR_FILE_NAME ist der Name der zu expandierenden Datei.
Das entsprechende Bedingungsmakro bleibt bis auf die verwendeten Namen gleich:
# define FOR_FILE_COND ( i ) ( FOR_FILE_START ) <= ( i ) && ((
FOR_FILE_START ) +( FOR_FILE_COUNT ) ) > ( i )
Sebastian Otte, Fachhochschule Wiesbaden
Seite 32
C++ Metaprogrammierung
3 Präprozessor Metaprogrammierung
Die Implementierung der Iterationsschritte gestaltet sich nun deutlich anders:
# if FOR_FILE_COND (0)
# define FOR_FILE_VALUE 0
# include FOR_FILE_NAME
# undef FOR_FILE_VALUE
# endif
...
# if FOR_FILE_COND (19)
# define FOR_FILE_VALUE 19
# include FOR_FILE_NAME
# undef FOR_FILE_VALUE
# endif
Innerhalb eines Iterationsschritts wird kurzzeitig ein Makro FOR_FILE_VALUE erzeugt,
welches den Zählerwert des aktuellen Durchlaufs beinhaltet. Das ist notwendig, damit
innerhalb der zu expandierenden Datei, die in FOR_FILE_NAME festgehalten ist, auf den
aktuellen Zählerwert zugegriffen werden kann.
Die zu expandierende Datei testfile.h in diesem Bespiel hat folgenden Inhalt:
printf ( " % dThis is a test file " , FOR_FILE_VALUE ) ;
Eine entsprechende Anwendung des Verfahrens könnte wie folgt aussehen:
int
{
#
#
#
main ()
#
include " for_file . h "
define FOR_FILE_START 0
define FOR_FILE_COUNT 20
define FOR_FILE_NAME " testfile . h "
return 0;
}
Wieder werden vor dem Einfügen der Iteration die Schleifenparameter konfiguriert.
Durch Inkludieren der Schleifendatei wird die Iteration gestartet. Für das Beispiel liefert
der Präprozessor die Ausgabe:
// after preprocessing
int main ()
{
printf ( " % dThis is a test file " , 0) ;
printf ( " % dThis is a test file " , 1) ;
printf ( " % dThis is a test file " , 2) ;
Sebastian Otte, Fachhochschule Wiesbaden
Seite 33
C++ Metaprogrammierung
3 Präprozessor Metaprogrammierung
...
printf ( " % dThis is a test file " , 17) ;
printf ( " % dThis is a test file " , 18) ;
printf ( " % dThis is a test file " , 19) ;
return 0;
}
3.1.5 Selbst Iteration
Die sogenannte selbst Iteration läßt sich als Variante der Datei Iteration verstehen. Dabei
befindet sich der zu expandierende Code nicht in einer zusätzlichen Datei, sondern innerhalb der Datei, in der die Iteration initialisiert wird. Das macht es allerdings notwendig,
in der Datei die Bereiche zu markieren, über die iteriert und über die nicht iteriert werden
soll. Um dies zu ermöglichen, muss die Schleifendatei (for_file.h) aus dem vorherigen
Beispiel erweitert werden. Zu Beginn der Datei muss das Flag FOR_FILE_ITERATING definiert und am Ende der Datei mit #undef wieder gelöscht werden. FOR_FILE_ITERATING
ist also nur während der Iteration gültig. Das Flag lässt sich wie folgt verwenden:
# if ! defined ( FOR_FILE_ITERATING )
int main ()
{
#
define FOR_FILE_START 0
#
define FOR_FILE_COUNT 20
#
define FOR_FILE_NAME " iteration_self . cpp "
#
include " for_file . h "
# endif
# if defined ( FOR_FILE_ITERATING )
printf ( " % dThis is a self iteration test file " ,
FOR_FILE_VALUE ) ;
# endif
# if ! defined ( FOR_FILE_ITERATING )
}
# endif
Dieses Beipsiel ist ähnlich aufgebaut wie das Bespiel der Datei Iteration, mit dem Unterschied, dass für FOR_FILE_NAME der eigene Dateiname angegeben wurde. Zusätzlich
ist die Datei in unterschiedliche Bereiche eingeteilt. Die Bereiche, die mit #if !defined
(FOR_FILE_ITERATING) beginnen, werden vom Präprozessor ausgegeben, wenn gera-
Sebastian Otte, Fachhochschule Wiesbaden
Seite 34
C++ Metaprogrammierung
3 Präprozessor Metaprogrammierung
de keine Iteration läuft (das ist genau einmal). Die Bereiche, die durch #if defined(
FOR_FILE_ITERATING) gekennzeichnet sind, werden bei der Iteration expandiert.
Achtung: Bereiche, die nicht gekennzeichnet sind, tauchen sowohl innerhalb, als auch
außerhalb der Iteration auf.
Hier die Ausgabe des Präprozessors:
// after preprocessing
int main ()
{
printf ( " % dThis is a self iteration test file " , 0) ;
printf ( " % dThis is a self iteration test file " , 1) ;
printf ( " % dThis is a self iteration test file " , 2) ;
...
printf ( " % dThis is a self iteration test file " , 17) ;
printf ( " % dThis is a self iteration test file " , 18) ;
printf ( " % dThis is a self iteration test file " , 19) ;
}
Sebastian Otte, Fachhochschule Wiesbaden
Seite 35
C++ Metaprogrammierung
4 Template Metaprogrammierung
4 Template Metaprogrammierung
4.1 Basistechniken
Der folgende Abschnitt soll zunächst einen Überlick über wichtige Grundlagen der Template Metaprogrammierung liefern. Einige der hier aufgeführten Techniken finden in
späteren Abschnitten Anwendung und helfen die Funktionsweise fortgeschrittener Konzepte zu erläutern.
4.1.1 Funktionen
Bei der Template Metaprogrammierung werden Funktionen benötigt, die ein Funktionsergebnis bereits zur Übersetzungszeit liefern. Das ist jedoch über klassische C/C++
Funktionsdefinitionen kaum möglich. Daher bedient man sich eines Tricks: Mit den Funktionsargumenten wird ein Klassentemplate parametrisiert. Über einen konstanten Member (hier einelementige Enumeration) kann das Ergebnis der Funktion statisch wieder
extrahiert werden.
Sei die Funktion id : N → N mit id(n) = n, dann läßt sich diese Funktion als Template
wie folgt formulieren:
template < unsigned int x >
struct id
{
enum { value = x };
};
Aufgerufen werden kann die Funktion zum Beispiel durch den Ausdruck id<10>::value.
Dabei ist es Konvention, das Ergebnis über ::value aus dem Template zu extrahieren.
Natürlich sind auch mehrere Parameter möglich.
Sei die Funktion add : N2 → N mit add(x, y) = x + y, dann kann ein entsprechendes
Template lauten:
template < unsigned int x , unsigned int y >
struct add
{
enum { value = x + y };
};
Ergebnisse, der durch Templates implementierten Funktionen würden völlig statisch,
während der Übersetzung eines Programmes (welches die Funktionen aufruft) erzeugt
werden. Der Compiler ist dabei in der Lage, größtenteils alle arithmetischen, logischen
Sebastian Otte, Fachhochschule Wiesbaden
Seite 36
C++ Metaprogrammierung
4 Template Metaprogrammierung
und binären Operationen auszuwerten.
Ganz allgemein gilt, dass jede Funktion f : Nn : N auch als entsprechendes Template
template < unsigned int x1 , ... , unsigned int xn >
struct f
{
enum { value = expression };
};
darstellbar ist. expression kann dabei ein beliebiger Ausdruck sein und auch durch
Templates implementierte Funktionen verwenden. Auch sei anzumerken, dass die Parameter nicht auf den Datentyp unsigned int beschränkt sind, sondern alle diskreten
Typen verwenden können (siehe Grundlagen 2.3.3).
4.1.2 Enumeration vs. konstante Klassenvariable
Immer wieder wird darüber diskutiert, ob die Ergebnispräsentation in Template-Funktionen
über eine einelementige Enumeration oder vielleicht doch besser über eine konstante
Klassenvariable geschehen sollte:
template < int x >
struct id_enum
{
enum { value = x };
};
template < int x >
struct id_static
{
static const int value = x ;
};
Eigentlich würde man denken, dass static const int der bessere Weg sei, da konstante Klassenvariablen ein neueres Feature sind und es einfach sauberer aussieht, als
der Trick mit der Enumerationen. Letztlich ist jedoch genau des Gegenteil der Fall. Das
ausschlaggebende Argument ist hier nicht die mögliche Abwärtskompatibilität von Enumerations, sonder vielmehr folgender Sachverhalt (nach [VJ07], S. 303ff):
Templates sind, wie in den Grundlagen beschrieben, ein generisches Konzept, welches
erst zur Übersetzungszeit tatsächlichen Code erzeugt. Ein Klassentemplate erzeugt eine
Klasse also erst dann, wenn es parametrisiert wird.
Klassenvariablen, ob konstant oder nicht, sind sogenannte lvalues1 . Wird ein Klassentemplate parametrisiert, das Klassenvariablen enthält, veranlasst dies den Compiler
1
lvalues sind Variablen, die eine Adresse haben und in einem bestimmten Speicherbereich angelegt
werden müssen.
Sebastian Otte, Fachhochschule Wiesbaden
Seite 37
C++ Metaprogrammierung
4 Template Metaprogrammierung
dazu, diese im statischen Speicher anzulegen und zu instanzieren.
Werte von Enumerations sind jedoch keine lvalues, haben also keine Adresse, und
werden vom Compiler behandelt wie Literale. Da sich nach Definition der Template Metaprogrammierung diese ausschließlich auf Berechnungen bzw. Verarbeitung zur
Übersetzungszeit bezieht und der Effekt von Klassenvariablen über die Übersetzungszeit hinausgeht, sind Enumerations zu bevorzugen.
4.1.3 Type Functions
Type Functions meinen allgemein Funktionen, die anstelle eines Wertes einen Datentypen zurückgeben oder ihr Ergebnis von einem Datentypen abhängig machen. Eine
klassische „build-in“ Typ-Funktion der Sprache C ist der sizeof-Operator. Das Funktionserbnis ist abhängig vom übergebenen Datentyp. In der Template Metaprogrammierung bezeichnet eine Typ-Funktion ein Template, aus dem sich ein Datentyp extrahieren
lässt. Zur Veranschaulichung ein kleines Beispiel:
template < typename arg >
struct id_type
{
typedef arg type ;
};
Das Klassentemplate id_type stellt durch typedef den Datentypen außerhalb des Templates zur Verfügung, mit dem es parametrisiert wurde. Über den Aufruf id_type<int
>::type lässt sich der Datentyp (hier int) aus dem Template extrahieren.
Das nachfolgende Template zeigt eine Typ-Funktion, die abhängig von einem NichttypParemeter einen Typ liefert:
template < int bits >
struct number_type
{
typedef int type ;
};
template <>
struct number_type <16 >
{
typedef short type ;
};
template <>
struct number_type <8 >
{
typedef char type ;
Sebastian Otte, Fachhochschule Wiesbaden
Seite 38
C++ Metaprogrammierung
4 Template Metaprogrammierung
};
Das Template number_type wird mit einem Wert des Typs int parametrisiert. Es soll,
je nach übergebenem Wert (der hier die benötigte Bitgröße darstellt) einen entsprechenden Ganzzahlen-Datentyp liefern. In diesem Beispiel sind die konkreten Fälle 16
(number_type liefert short) und 8 (number_type liefert char) definiert. In jedem anderen Fall gibt das Template int zurück.
Mit Templates ist es auch möglich, Datentypen, die wiederum aus Templates stammen,
weiterzureichen, wie das nächste Beispiel zeigt:
template < typename arg >
struct bitsize
{
enum { value = sizeof ( arg ) * 8 };
};
template < typename arg >
struct bigger_type
{
typedef typename number_type < bitsize < arg >:: value *
2 >:: type type ;
};
Mit Hilfe des Templates bigger_type kann über Angabe eines Datentypens (es wird
zur Vereinfachung angenommen, es handle sich um einen Ganzzahl-Datentypen) der
nächstgrößere Typ ermittelt werden. Dazu wird zunächst die Bitgröße des übergebenen
Datentypens durch das Template bitsize bestimmt. Mit dem verdoppelten Ergebnis
wird das oben behandelte Template number_type parametrisiert. Beim „Auffangen“ des
resultierenden Typens ist das Schlüsselwort typename nach typedef notwendig, um dem
Compiler mitzuteilen, dass der folgende Template-Ausdruck einen Datentypen liefert und
er als solcher interpretiert werden kann.
Der Aufruf bigger_type<char>::type beispielsweise, gibt den Datentyp short zurück.
4.1.4 Rekursionen
Im Abschnitt 2.3.7 war die Rede davon, dass C++ Templates Turing-vollständig sind.
Darauf folgt unweigerlich, dass es auch etwas wie Schleifen geben muss. Nun sind klassische (iterierende) Schleifen mit Templates leider nicht formulierbar. Das liegt unter
anderem daran, dass „Variablen“ in Templates ihren Wert nach der ersten Zuweisung
nicht mehr ändern können. Dafür lassen sich mit Templates jedoch zu Schleifen äquivalente Rekursionen programmieren.
Eine solche Rekursion lässt sich dadurch konstruieren, dass ein Template sich selbst instanziiert und das solange, bis ein gewünschter Zustand eingetroffen ist. Üblicherweise
Sebastian Otte, Fachhochschule Wiesbaden
Seite 39
C++ Metaprogrammierung
4 Template Metaprogrammierung
verwenden rekursive Templates Ganzzahlen als Parameter.
Ein Rekursionsabbruch wird durch die Spezialisierung des Templates für den entsprechenden Fall definiert. Interessant ist, dass sich rekursive Templates ähnlich formulieren
lassen, wie formale induktive Funktionsdefinitionen. Sei als Beispiel die induktive Variante der Fakultät einer Zahl n ∈ N formal gegeben durch:
(
1,
falls n = 0
n! =
n ∗ (n − 1)!, falls n > 0
Diese Definition lässt sich nun als Template wie folgt ausdrücken:
template < unsigned int n >
struct factorial
{
enum { value = n * factorial < n - 1 >:: value };
};
template <>
struct factorial <0 >
{
enum { value = 1 };
};
Das erste Template definiert hier den Rekursionsschritt n ∗ (n − 1)!. Dabei instanziiert es
solange sich selbst, jeweils mit veringertem n um 1, bis der Fall n = 0, der hier durch das
zweite (spezialisierte) Template mit factorial<0> gegeben ist, eintritt. Der Ausdruck
factorial<5>::value beispielsweise wird aufgelöst, wie im Folgenden geschildert:
factorial<5>::value = 5 ∗ factorial<4>::value
{z
}
|
4 ∗ factorial<3>::value
|
{z
}
3 ∗ factorial<2>::value
|
{z
}
2 ∗ factorial<1>::value
|
{z
}
1 ∗ factorial<0>::value
|
{z
}
1
factorial<5>::value = 5 ∗ 4 ∗ 3 ∗ 2 ∗ 1 ∗ 1 = 120
Bei der Übersetzung durch den Compiler wird der Ausdruck factorial<5>::value
durch 120 ersetzt. Im übersetzten Programm ist dann von der verschachtelten Templatestruktur nichts mehr übrig.
Durch Templates konstruierte Rekursionen sind ein elementares und mächtiges Werkzeug der Template Metaprogrammierung und findet Anwendung bei einer Vielzahl von
fortschreitenden Konzepten.
Sebastian Otte, Fachhochschule Wiesbaden
Seite 40
C++ Metaprogrammierung
4 Template Metaprogrammierung
4.1.5 Rekusionen und bedingte Verzweigung
Unter einer bedingten Verzweigung versteht man in klassischem C/C++ eine Verzweigung im Programmfluss durch beispielsweise ein if-else-Konstrukt oder den bedingten Ausdruck1 . Die Auswertung der Bedingung geschieht hierbei erst zur Laufzeit des
Programms. Wie bereits erwähnt, sollen aber solche Nach-Compilezeit-Effekte in der
Template Metaprogrammierung vermieden werden. Tatsächlich gibt es mehrere Möglichkeiten, bereits zur Übersetzungszeit Bedingungen auszuwerten und abhängig davon
zu „verzweigen“. Ein Vorschlag wäre, eine Entscheidung wie folgt auf ein Template abzubilden (vgl. [VJ07] S. 272 ff.):
template < bool cond , int true_part , int false_part >
struct IfThenElse ;
template < int true_part , int false_part >
struct IfThenElse < true , true_part , false_part >
{
enum { value = true_part };
};
template < int true_part , int false_part >
struct IfThenElse < false , true_part , false_part >
{
enum { value = false_part };
};
Die Funktionsweise des IfThenElse-Templates ist recht simpel. Es sind zwei Spezialisierungen definiert, wobei die erste den Fall cond = true und die zweite den Fall cond
= false betrachtet. Tritt der erste Fall ein (die Bedingung ist wahr), ist value definiert
als true_part. Im zweiten Fall (die Bedingung ist nicht wahr), ist value definiert als
false_part.
Mit dem Template IfThenElse ist es möglich, während der Übersetzungszeit Bedingungen auszuwerten und entsprechend darauf zu reagieren. Beispielsweise lassen sich mit
dessen Hilfe Templates konstruieren, die in der Lage sind, zu entscheiden, ob eine Zahl
eine Primzahl ist. Dazu sei zunächst ein Template zu formulieren, das der folgenden
formalen Funktiondefinition Pcheck : N2 → {0, 1} entspricht (es gilt i < n):

falls i = 1

1,
Pcheck (i, n) = 0,
falls i teilt n


Pcheck (i − 1, n), sonst
Das entsprechende Template is_prim_check kann wie folgt aussehen:
1
cond ? true_part : false_part liefert, wenn cond wahr ist true_part, ansonsten false_part
wobei cond ein beliebiger logischer Ausdruck sein kann.
Sebastian Otte, Fachhochschule Wiesbaden
Seite 41
C++ Metaprogrammierung
4 Template Metaprogrammierung
template < int i , int n >
struct is_prim_check
{
enum { value = IfThenElse < (( n % i ) != 0) ,
( is_prim_check < i - 1 , n >:: value ) ,
(0) >:: value };
};
template < int n >
struct is_prim_check <1 , n >
{
enum { value = 1 };
};
Das Template kombiniert die Techniken der rekursiven Templatedefinition aus dem vorherigen Abschnitt und die bedingte Verzweigung mit Hilfe von IfThenElse, um die
Anforderung von Pcheck zu erfüllen. is_prim_check arbeitet nun wie folgt: Parametrisiert man das Template mit zwei Zahlen i und n, wobei i kleiner sein muss als n,
instanziiert das Template solange sich selbst, bis entweder i = 1 ist (Spezialisierung
von is_prim_check) oder bis i ein Teiler von n ist.
Jetzt wird noch eine Funktion benötigt, die Pcheck korrekt verwendet. Sei dafür die Funktion P : N → {0, 1} gegeben:
(
Pcheck (n ÷ 2, n), falls n > 1
P (n) =
0,
falls n ≤ 1
Die Funktion P (n) liefert als Ergebnis 0, wenn n keine Primzahl ist. Das Ergebnis ist 1,
wenn n eine Primzahl ist. Ein Template, das genau dies leistet, könnte folgendermaßen
formuliert sein:
template < int n >
struct is_prim
{
enum { value = is_prim_check <( n / 2) , n >:: value };
};
template <>
struct is_prim <1 >
{
enum { value = 0 };
};
template <>
Sebastian Otte, Fachhochschule Wiesbaden
Seite 42
C++ Metaprogrammierung
4 Template Metaprogrammierung
struct is_prim <0 >
{
enum { value = 0 };
};
Damit ist die Konstruktion fertig. Das Template is_prim liefert für eine übergebene
Zahl nun die Information, ob sie eine Primzahl ist. Zum Beispiel liefert is_prim<131>::
value eine 1, während aus is_prim<133>::value eine 0 resultiert.
Eine weitere Variante bedingte Verzweigungen in Templates umzusetzen, ist der oben
bereits erwähnte bedingte Ausdruck. Benutzt man diesen in Templates und ist die Bedingung nur abhängig von statischen Konstanten, wird der Ausdruck durch den Compiler
komplett ersetzt und verhält sich damit ähnlich wie das IfThenElse-Template.
Achtung: Sowohl für das IfThenElse-Template als auch den bedingten Ausdruck gilt,
dass beide Zweige komplett instanziert werden, auch wenn ein Zweig durch die Bedingung ausgeschlossen ist. Dies soll folgendes Beispiel veranschaulichen:
template < int n >
struct endless
{
enum { value = endless < n + 1 >:: value };
};
template < int x >
struct test1
{
enum { value = ( x > 0) ?( x ) :( endless <x >:: value ) };
};
template < int x >
struct test2
{
enum { value = IfThenElse <( x > 0) , ( x ) ,
( endless <x >:: value ) >
:: value };
};
...
test1 <10 >:: value
test2 <10 >:: value
Sebastian Otte, Fachhochschule Wiesbaden
Seite 43
C++ Metaprogrammierung
4 Template Metaprogrammierung
Beide Parametrisierungen test1<10> und test2<10> führen zu einem Compilerfehler.
Obwohl man meinen sollte, dass durch die Bedingung x > 0 mit x = 10 sichergestellt
ist, dass das Template endless nicht erreicht wird, läuft der Compiler dennoch auch
durch den zweiten Zweig und dabei in die Endlosschleife von endless.
Die Möglichkeit in Templates bedingte Verzweigungen verwenden zu können, ist für die
Template Metaprogrammierung von erheblichem Wert. Nebenbei sei angemerkt, dass
bedingte Verzweigungen in Verbindung mit Rekursionen der Template Programmierung
erst zur Turing-Vollständigkeit1 verhelfen.
1
Durch Auswertung von Bedingungen kann der µ-Operator der partiell rekursiven Funktionen konstruiert werden.
Sebastian Otte, Fachhochschule Wiesbaden
Seite 44
C++ Metaprogrammierung
4 Template Metaprogrammierung
4.2 Fortgeschrittene Konzepte
Während des vorherigen Abschnitts wurden einige grundlegende Elemente der Template
Metaprogrammierung erläutert. Dieser Abschnitt befasst sich nun mit fortgeschrittenen
Konzepten, die auf den vorgestellten Basistechniken aufbauen.
4.2.1 Unrolled Loops
Nicht selten kommt es vor, dass für bestimmte Berechnungen, insbesondere bei der
Verwendung dynamischer Datenstrukturen, Schleifen zum Einsatz kommen. In zeitkritischen Anwendungen werden Schleifen häufig, wenn es denn möglich ist, aufgelöst
„aufgerollt“. Aufrollen meint, dass die Instruktionen innerhalb einer Schleife wiederholt
untereinander geschrieben werden, so dass auf die Schleife verzichtet werden kann. Dadurch kann die Anzahl der ingesamt ausgeführten Instruktionen stark reduziert werden,
da sämtliche Kontrollinstruktionen der Schleife wegfallen. Außerdem gibt es bei reinen
Berechnungen keine Sprünge mehr, was die Ausführungsgeschwindigkeit zusätzlich erhöht.
Ein Nachteil an manuell aufgerollten Schleifen ist, dass sie die Dynamik und Portabilität
des Codes sehr einschränken. Beispielsweise entstünde hohe Rendundanz von Algorithmen für unterschiedliche Konfigurationen1 .
Durch Template Metaprogrammierung können Schleifen modelliert werden, die eine Allgemeingültigkeit und bei Verwendung zur Entwicklungszeit einen dynamischen Charakter haben. Nach der Übersetzung solchen Codes, erzeugt der Compiler dann schleifenlose,
im Optimalfall minimale Instruktionssequenzen. Ein anschauliches Beispiel für ein solches Template liefern die nachfolgenden Listings (vgl. [VJ07] S. 314 ff).
Ausgangspunkt dieses Beispiels ist eine Funktion zur Berechnung der Skalarprodukts
(Punktprodukts) zweier Vektoren ~x, ~y ∈ Rn . Das Skalarprodukt ist formal wie folgt
definiert:
~x · ~y =
n
X
xi yi = x1 y1 + x2 y2 + x3 y3 + · · · + xn yn
i=1
Eine entsprechende Implementierung für einen beliebigen Datentyp könnte mit einer
Schleife folgendermaßen aussehen:
template < typename T >
inline T dot_product ( T * a , T * b , int dim )
{
T result = T () ;
for ( int i = 0; i < dim ; i ++)
{
1
Konkrete Datentypen oder Felder mit konkreter Länge
Sebastian Otte, Fachhochschule Wiesbaden
Seite 45
C++ Metaprogrammierung
4 Template Metaprogrammierung
result += a [ i ] * b [ i ];
}
return result ;
}
Das ist sicher eine schlanke Lösung des Problems, jedoch wäre eine aufgerollte Variante
letztendlich noch schlanker. Die Frage ist nur, wie sich die Schleife aufrollen lässt, ohne
auf den dynamischen Aspekt der variablen Dimensionierung verzichten zu müssen. Die
Idee ist es, mit Hilfe von rekursiven Templates die Schleife nachzubilden.
template < int N , typename T >
struct dotproduct_s
{
static T result ( T * a , T * b )
{
return (* a ) * (* b ) + dotproduct_s < N - 1 , T >::
result ( a + 1 , b + 1) ;
};
};
Obwohl das Template auf den ersten Blick umständlich aussieht, ist es doch geschickt
formuliert. Die statische Funktion result erwartet zwei Zeiger auf die entsprechenden
Vektoren. Zunächst werden die jeweils ersten Vektorelemente verechnet (multipliziert).
Dann folgt der rekursive Aufruf, bei dem die angenommene Dimension der Vektoren um
1 reduziert wird. Gleichzeitig werden als Referenzvektoren die ursprünglichen Vektoren
ohne das erste Element angeben. Beendet wird die Rekursion durch Spezialisierung des
Templates für die Betrachtung von 1-dimensionalen Vektoren.
template < typename T >
struct dotproduct_s <1 , T >
{
static T result ( T * a , T * b )
{
return (* a ) * (* b ) ;
};
};
Um den Aufruf des Templates etwas zu erleichtern, wird noch eine kleine Hilfsfunktion
verwendet, die das Instanziieren und die Extraktion des Ergebnisses kapselt.
template < int N , typename T >
inline T dotproduct ( T * a , T * b )
{
return dotproduct_s <N , T >:: result (a , b ) ;
}
Sebastian Otte, Fachhochschule Wiesbaden
Seite 46
C++ Metaprogrammierung
4 Template Metaprogrammierung
Durch Parametrisierung der Funktion dotproduct wird das Template dotproduct_s
entsprechend instanziiert und der gewünschte Algorithmus entsteht ohne Schleife zur
Übersetzungszeit. Um dies zu überprüfen, wurden die beiden Funktionen (mit Schleife
und ohne Schleife) wie folgt aufgerufen:
int main ()
{
int a [3] = {1 , 2 , 3};
int b [3] = {4 , 5 , 6};
std :: cout << dot_product (a , b , 3) << std :: endl ;
std :: cout << dotproduct <3 >( a , b ) << std :: endl ;
return 0;
}
Beide Aufrufe liefern hier das gleiche Ergebnis, nämlich 32. Nun stellt sich die Frage,
wie sich die übersetzten Funktionen im einzelnen unterscheiden. Eine Möglichkeit dies
herauszufinden, ist die Analyse des resultierenden Assemblercodes:
Listing 4.1: Mit Schleife (dot_product)
push
mov
push
push
push
mov
mov
mov
mov
mov
cmp
jge
ebp
ebp ,
edi
esi
ebx
edi ,
esi ,
ebx ,
ecx ,
edx ,
ecx ,
L32
mov
imul
add
inc
cmp
jl
eax ,
eax ,
ecx ,
edx
edx ,
L30
mov
pop
pop
pop
pop
ret
eax , ecx
ebx
esi
edi
ebp
esp
DWORD PTR [ ebp +8]
DWORD PTR [ ebp +12]
DWORD PTR [ ebp +16]
0
0
ebx
L30 :
DWORD PTR [ edi + edx *4]
DWORD PTR [ esi + edx *4]
eax
Listing 4.2: Ohne Schleife (dotproduct)
push
mov
push
mov
mov
mov
imul
mov
imul
mov
imul
add
add
pop
pop
ret
ebp
ebp ,
ebx
edx ,
ebx ,
eax ,
eax ,
ecx ,
ecx ,
edx ,
edx ,
ecx ,
eax ,
ebx
ebp
esp
DWORD
DWORD
DWORD
DWORD
DWORD
DWORD
DWORD
DWORD
edx
ecx
PTR
PTR
PTR
PTR
PTR
PTR
PTR
PTR
[ ebp +8]
[ ebp +12]
[ edx ]
[ ebx ]
[ edx +4]
[ ebx +4]
[ edx +8]
[ ebx +8]
ebx
L32 :
Der direkte Vergleich macht deutlich, wie gravierend der Unterschied ist, besonders wenn
man bedenkt, dass die Instruktionssequenz zwischen den Marken L30 und L32 im linken
Listing drei mal ausgeführt wird.
Sebastian Otte, Fachhochschule Wiesbaden
Seite 47
C++ Metaprogrammierung
4 Template Metaprogrammierung
4.2.2 Expression Templates
Expression Templates sind wahrscheinlich das Paradebeispiel für Template Metaprogrammierung. Ganz allgemein bezeichnet man Templates als Expression Templates,
wenn sie gezielt Ausdrücke manipulieren oder gar erst ermöglichen können. So lassen
sich sogar u.a. mit Hilfe von Expression Templates völlig neue Sprachen modellieren,
die innerhalb von C++ verwendet werden können. Man spricht hier von sogenannten
DSLs (domain specific languages) oder DSELs (domain specific embedded languages).
Das sind Sprachen, die meist einen hohen Abstraktionsgrad und eine hohe Aussagekraft
in Bezug auf konkrete Problembereiche aufweisen (vgl. [Hof05] S. 6).
Das folgende Beispiel soll zunächst einen Einstieg in die Funktionsweise von Expression
Templates liefern:
template < typename type , int size >
struct array
{
type data [ size ];
type & operator []( const int i )
{ return data [ i ]; }
};
Das Template array liefert eine Struktur, die ein Array vom Datentyp type mit size
Elementen enthält. Will man nun die Werte eines solchen bereits instanziierten Arrays
manipulieren, geht das nur über den []-Operator (Index-Operator), zum Beispiel:
array < int , 10 > a ;
a [0] = 0;
a [1] = 1;
...
a [9] = 9;
Manchmal, inbesondere bei vielen Elementen, kann dies sehr mühselig und unübersichtlich sein. Beispielsweise wäre es doch praktisch, könnte man einfach schreiben
a = 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9;
und das Array würde entsprechend mit den Werten in der angebenen Reihenfolge befüllt. Mit Hilfe von Expression Templates ist dies möglich.
Für den nachfolgenden Lösungsansatz ist es notwendig, den obigen Ausdruck Schritt für
Schritt zu zerlegen. Dabei ist es wichtig zu wissen, dass der Compiler den ,-Operator
von links nach rechts interpretiert.
Der erste Schritt besteht nun darin, den Teilausdruck a = 0 zu verarbeiten. Dazu wird
zunächst das folgende Hilfstemplate definiert:
Sebastian Otte, Fachhochschule Wiesbaden
Seite 48
C++ Metaprogrammierung
4 Template Metaprogrammierung
template < typename type , int idx >
struct array_expr
{
type * ref_data ;
array_expr ( type * ref ,
const type & value ) : ref_data ( ref )
{ ref_data [ idx ] = value ; }
};
Ein instanziiertes array_expr Template kapselt die Zuweisung eines bestimmten Wertes
vom Typ type an den Index idx eines übergebenen Arrays (hier Pointer). Array und
Wert werden dem Konstruktor der Datenstruktur übergeben, in dem augenblicklich die
Zuweisung stattfindet. Nun kann das Template array um den =-Operator (ZuweisungsOperator) erweitert werden und zwar in der Form:
template < typename type , int size >
struct array
{
type data [ size ];
type & operator []( const int i )
{ return data [ i ]; }
array_expr < type , 0 >
operator =( const type & value )
{ return array_expr < type , 0 >
( data , value ) ; }
};
Durch den Zuweisungsoperator im Template array ist der Compiler in der Lage, den
Ausdruck a = 0 aufzulösen, dabei liefert eine Zuweisung als Ergebnistyp eine Instanz
der durch das Template array_expr erzeugten Datenstruktur. Der Aufruf a = 0 führt
nun dazu, dass die 0 als erstes Element der internen Arrays von a gesetzt wird. Der
exemplarische Ausdruck
a = 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9;
wird nun ersetzt durch den Ausdruck
array_expr , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9
wobei array_expr hier eine konkrete Objektinstanz der Datenstruktur meint.
Der nächste Schritt ist jetzt im Template array_expr den ,-Operator zu überladen, um
Zuweisung fortzuführen. Hierfür muss das Template wie folgt erweitert werden:
Sebastian Otte, Fachhochschule Wiesbaden
Seite 49
C++ Metaprogrammierung
4 Template Metaprogrammierung
template < typename type , int idx >
struct array_expr
{
type * ref_data ;
array_expr ( type * ref ,
const type & value ) : ref_data ( ref )
{ ref_data [ idx ] = value ; }
array_expr < type , idx + 1 >
operator ,( const type & value )
{ return array_expr < type , idx + 1 >
( ref_data , value ) ; }
};
Beim Aufruf des ,-Operators wird eine neue Objektinstanz von array_expr erzeugt.
Dabei werden der bereits gespeicherte Zeiger auf das interne Array der vorausgegangenen array Instanz und der an den ,-Operator übergebene Wert an den Konstruktur
von array_expr überreicht. Wieder findet bei der Instanziierung die eigentliche Wertzuweisung statt. Dieses Mal wird jedoch der Wert dem Element an der Stelle idx + 1
des internen Arrays zugewiesen, da das Template array_expr mit idx + 1 parametrisiert wurde. Dadurch, dass der ,-Operator wieder eine Instanz von array_expr liefert,
kann der Compiler auch den restlichen Ausdruck auflösen. Die komplette Auflösung des
gesamten Ausdrucks gestaltet sich nun wie folgt:
a
= 0} , 1, 2, 3, 4, 5, 6, 7, 8, 9
−→ array_expr<int, 0>
| {z
array_expr, 1 , 2, 3, 4, 5, 6, 7, 8, 9 −→ array_expr<int, 1>
|
{z
}
array_expr, 2 , 3, 4, 5, 6, 7, 8, 9
−→ array_expr<int, 2>
|
{z
}
array_expr, 3 , 4, 5, 6, 7, 8, 9
−→ array_expr<int, 3>
|
{z
}
array_expr, 4 , 5, 6, 7, 8, 9
−→ array_expr<int, 4>
|
{z
}
array_expr, 5 , 6, 7, 8, 9
−→ array_expr<int, 5>
|
{z
}
array_expr, 6 , 7, 8, 9
−→ array_expr<int, 6>
{z
}
|
−→ array_expr<int, 7>
array_expr, 7 , 8, 9
|
{z
}
array_expr, 8 , 9
−→ array_expr<int, 8>
|
{z
}
array_expr, 9
−→ array_expr<int, 9>
|
{z
}
array_expr
Sebastian Otte, Fachhochschule Wiesbaden
Seite 50
C++ Metaprogrammierung
4 Template Metaprogrammierung
Nach der letzten Verarbeitung des ,-Operators wurde auch der letzte Wert des Ausrucks
(in diesem Fall 9) dem entsprechenden Element des internen Arrays zugewiesen.
Der nachfolgende Code zeigt die Anwendung des konstruierten Expression Templates.
int main ()
{
array < int , 10 > a ;
a = 0;
a = 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9;
a = 3 , 2 , 1;
for ( int i = 0; i < 10; i ++)
{
std :: cout << a [ i ] << std :: endl ;
}
}
Alle drei Zuweisungen an a werden vom Compiler nun als gültige Ausdrücke interpretiert
und dabei ist die Verarbeitung durch die Templates völlig unsichtbar. Das Programm
liefert die Ausgabe:
3
2
1
3
4
5
6
7
8
9
Wie bereits erwähnt gibt es mehrere Möglichkeiten Expressionen Templates zu implementieren. Entscheidend ist jedoch, dass Expression Templates einen Teilausdruck oder
einen gesamten Ausdruck repräsentieren können und dabei für den Anwender unsichtbar
bleiben.
In [Arc01] illustriert Thomas Arce sehr eindrucksvoll, wie mit Hilfe von Expression
Templates Vektorrechnungen beschleunigt werden können.
Sebastian Otte, Fachhochschule Wiesbaden
Seite 51
C++ Metaprogrammierung
5 Anmerkungen und Ergänzungen
5 Anmerkungen und Ergänzungen
5.1 Kritik
Sowohl die Template Metaprogrammierung, als auch die Präprozessor Metaprogrammierung bieten ein scheinbar unerschöpfliches Maß an Möglichkeiten. Von Codeoptimierung,
Zeitersparnis durch weniger Tipparbeit bis hin zur Gestaltung völlig neuer Sprachmittel
bleiben kaum Wünsche offen. Aber bei aller praktischer Verwendbarkeit hat die Metaprogrammierung in C++, inbesondere die Template Metaprogrammierung nennenswerte
Nachteile:
• Schlechte Lesbarkeit der Quellcodes
Komplexe Template-Ausdrücke sind gerade für unerfahrene Programmierer äußerst schlecht lesbar und noch weniger gut zu warten. Ähnlich verhält es sich mit
der Präprozessor Metaprogrammierung, wie es am Beispiel der Datei-Iteration im
Abschnitt 3.1.4 deutlich wird.
• Entwicklung extrem aufwendig
Will man Konstrukte zur Metaprogrammierung selbst entwickeln, gestaltet sich
das meist als unheimlich aufwendig und setzt besonders bei der Template Metaprogrammierung ein weitreichendes Detailwissen voraus. Oft steht auch der Aufwand
der Entwicklung in keinem gerechtfertigten Verhältnis zum eigentlichen Nutzen.
• Templates schwer zu debuggen
Während Makros durch isolierte Ausführung des Präprozessors verhältnismäßig
leicht zu debuggen sind, ist dies bei Templates leider kaum möglich. Zum einen ist
es schwierig den resultierenden Zwischencode, der beim Übersetzen von Templates
entsteht, einzusehen und zum anderen sind Template bezogene Fehlermeldungen
oft sehr lang und unübersichtlich und enthalten meist keine brauchbare Fehlerbeschreibung.
• Compilerspezifische Probleme
Obwohl sich die hier verwendeten Techniken an gängige C/C++ Standards halten (z.B. des [C9903]), haben einige Compiler Probleme beispielsweise stark verschachtelte Templates zu übersetzen oder verhalten sich unterschiedlich. Neben
mangelnder Kompatibilität kann aber auch die hohe Anforderung an Rechenleistung während der Übersetzens problematisch werden.
Es sei auch noch einmal ausdrücklich erwähnt, dass weder C noch C++ als Metaprogrammiersprache entworfen wurde und sich die genannten Nachteile wohl aus eben dieser
Tatsache ergeben.
Sebastian Otte, Fachhochschule Wiesbaden
Seite 52
C++ Metaprogrammierung
5 Anmerkungen und Ergänzungen
5.2 Boost Bibliothek
Die Boost Bibliothek [DAR09] ist eine freie Sammlung von plattformunabhängigen
Unterbibliotheken für die Programmiersprache C++. Grundsätzlich dienen die darin
enthaltenen Bibliotheken dazu, die Verwendung von C++ zu vereinfachen oder auch
Geschwindigkeits-optimierte Codes zu schreiben. Das Bestreben ist die Steigerung der
Produktivität.
In der Boost Bibliothek gibt es zwei Teilbibliotheken, die insbesondere für die C++
Metaprogrammierung von besonderer Bedeutung sind.
5.2.1 BPL - Boost Preprocessor Library
Die BPL enthällt viele sehr mächtige Tools (zu meist Makros), die unter Verwendung
des Präprozessors arbeiten. Interessant ist, dass die BPL eine reine Header-Bibliothek
ist, d.h. alles beteiligten Dateien sind in -.h-Dateien ablegt.
Wie bereits erwähnt, sind auch viele der in diesem Dokument verwendeten Konzepte der
Präprozessor Metaprogrammierung durch die BPL inspiriert.
5.2.2 MPL - Meta Programming Library
Die MPL enthält vor allem Template-basierte Funktionalitäten und bietet ein breites
Spektrum an Template Funktionen und Datenstrukturen, sowie durch Templates definierte Ausdrücke (Expressions) für eine Vielzahl an Anwendungsmöglichkeiten. Dabei
ist MPL stark darauf ausgerichtet, bestimmte Probleme in einer problemorientierten abstrahierten Ebene zu behandeln, die sich nicht selten aus den alltäglichen C++ Sprachgewohnheiten heraushebt.
Die Boost Bibliothek ist inzwischen so weit verbreitet und geschätzt, dass viele ihrer
Teilbiblitheken nach dem C++-Standardisierungskomitees im nächsten C++-Standard
enthalten sein sollen.
Sebastian Otte, Fachhochschule Wiesbaden
Seite 53
C++ Metaprogrammierung
Index
Index
Symbols
,-Operator . . . . . . . . . . . . . . . . . . . . . . . . . . 48
::-Operator . . . . . . . . . . . . . . . . . . . . . . . . . 21
=-Operator . . . . . . . . . . . . . . . . . . . . . . . . . . 49
[]-Operator . . . . . . . . . . . . . . . . . . . . . . . . . 48
__VA_ARGS__ . . . . . . . . . . . . . . . . . . . . . . . . 12
class . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
lvalues . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38
struct . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
union . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
##-Operator . . . . . . . . . . . . . . . . . . . . . . . . . 13
#-Operator . . . . . . . . . . . . . . . . . . . . . . . . . . 12
einzeilige Expansion . . . . . . . . . . . . . . . . . 28
Enumeration . . . . . . . . . . . . . . . . . . . . . 21, 37
Erwin Unruh . . . . . . . . . . . . . . . . . . . . . . . . 24
Expansion . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
Expression Templates. . . . . . . . . . . . . . . .48
F
Fakultät . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40
Folgemakros . . . . . . . . . . . . . . . . . . . . . . . . . 27
Funktions-Rückgabe . . . . . . . . . . . . . . . . . 18
Funktions-Templates . . . . . . . . . . . . . . . . 18
Funktionsergebnis . . . . . . . . . . . . . . . . . . . 36
A
H
Allgemeingültigkeit . . . . . . . . . . . . . . . . . . 28
Anonyme Klassen. . . . . . . . . . . . . . . . . . . . .6
Ausdrücke . . . . . . . . . . . . . . . . . . . . . . . . . . . 48
Ausführungsgeschwindigkeit . . . . . . . . . 45
Horizontale Iteration . . . . . . . . . . . . . . . . 28
B
Baumstruktur . . . . . . . . . . . . . . . . . . . . . . . 16
bedingte Entscheidungen . . . . . . . . . . . . 18
bedingte Verzweigung . . . . . . . . . . . . . . . 41
bedingter Ausdruck . . . . . . . . . . . . . . . . . . 43
Boost Bibliothek . . . . . . . . . . . . . . . . . . . . 53
BPL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26, 53
D
Datei Iteration . . . . . . . . . . . . . . . . . . . . . . 32
DSEL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48
DSL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48
E
Sebastian Otte, Fachhochschule Wiesbaden
I
implizite Typisierung . . . . . . . . . . . . . . . . 19
Initialmakro . . . . . . . . . . . . . . . . . . . . . . . . . 28
Innere anonyme Klassen . . . . . . . . . . . . . . 7
Iteration. . . . . . . . . . . . . . . . . . . . . . . . . . . . .26
Iterationsbedingung . . . . . . . . . . . . . . . . . 31
Iterationskonzepte . . . . . . . . . . . . . . . . . . . 26
Iterationsmakro . . . . . . . . . . . . . . . . . . . . . 30
K
Klammern von Parametern . . . . . . . . . . 11
Klass . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
Klassen-Templates . . . . . . . . . . . . . . . . . . . 19
Konkatenation . . . . . . . . . . . . . . . . . . . . . . . 13
Konstante . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
konstante Klassenvariable. . . . . . . . . . . .37
L
Seite 54
C++ Metaprogrammierung
Lokale Iteration . . . . . . . . . . . . . . . . . . . . . 30
M
Makros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
Makrosubstitution . . . . . . . . . . . . . . . . . . . 27
mehrzeilige Expansion . . . . . . . . . . . . . . . 30
Mehrzeilige Makros . . . . . . . . . . . . . . . . . . . 9
meta . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
Metaebene. . . . . . . . . . . . . . . . . . . . . . . . . . . .5
Metaprogrammierung . . . . . . . . . . . . . . . . . 5
MPL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53
N
Nach-Compilezeit-Effekte . . . . . . . . . . . . 41
Namensraum . . . . . . . . . . . . . . . . . . . . . . . . . 7
Nicht-Typ Parameter . . . . . . . . . . . . . . . . 20
O
Objektinstanz . . . . . . . . . . . . . . . . . . . . . . . 20
P
Parameterliste . . . . . . . . . . . . . . . . . . . . . . . 12
Parametersubstitution . . . . . . . . . . . . . . . 10
partiell rekursive Funktionen . . . . . . . . 24
Partielle Spezialisierung . . . . . . . . . . . . . 23
Präprozessor . . . . . . . . . . . . . . . . . . . 5, 8, 26
Primitive horizontale Iteration . . . . . . . 26
Primzahlen . . . . . . . . . . . . . . . . . . . . . . . . . . 24
R
Rekursionen . . . . . . . . . . . . . . . . . . . . . . . . . 39
Rekursionsabbruch . . . . . . . . . . . . . . . . . . 40
Rekursionsschritt . . . . . . . . . . . . . . . . . . . . 40
rekursive Funktionen . . . . . . . . . . . . . . . . 24
Rekusionen . . . . . . . . . . . . . . . . . . . . . . . . . . 41
Rendundanz . . . . . . . . . . . . . . . . . . . . . . . . . 45
S
Sebastian Otte, Fachhochschule Wiesbaden
Index
Schablonen . . . . . . . . . . . . . . . . . . . . . . . . . . 18
Schleifen . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
Schleifendatei. . . . . . . . . . . . . . . . . . . . . . . .31
Schleifenparameter . . . . . . . . . . . . . . . . . . 32
Selbst Iteration . . . . . . . . . . . . . . . . . . . . . . 34
Sichtbarkeit . . . . . . . . . . . . . . . . . . . . . . . . . . 7
Skalarprodukt . . . . . . . . . . . . . . . . . . . . . . . 45
Spezialisierung . . . . . . . . . . . . . . . . . . . . . . 21
stringizing . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
Symbolsubstitution . . . . . . . . . . . . . . . . . . 10
T
Teilausdruck . . . . . . . . . . . . . . . . . . . . . . . . . 51
Template Metaprogrammierung . . . . . . 36
Token . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
transitive Parametersubstitution . . . . . 14
Turing-Maschine. . . . . . . . . . . . . . . . . . . . .24
Turing-Vollständigkeit . . . . . . . . 18, 24, 44
Typ-Deduktion . . . . . . . . . . . . . . . . . . . . . . 19
Typ-Parameter . . . . . . . . . . . . . . . . . . . . . . 19
Typ-Zuordnungen . . . . . . . . . . . . . . . . . . . 23
Type Functions . . . . . . . . . . . . . . . . . . . . . . 38
U
Uebersetzungszeit . . . . . . . . . . . . . . . . 24, 41
Unrolled Loops . . . . . . . . . . . . . . . . . . . . . . 45
V
Variadic Macros . . . . . . . . . . . . . . . . . . . . . 11
Verschachtelte Makros . . . . . . . . . . . . . . . 14
verschachtelte Makrosubsitution . . . . . 29
W
Wiederverwendbarkeit . . . . . . . . . . . . . . . 28
Z
Zeichenkette . . . . . . . . . . . . . . . . . . . . . . . . . 12
Seite 55
C++ Metaprogrammierung
Index
Zeilenumbruch . . . . . . . . . . . . . . . . . . . . . . . . 9
zeitkritisch . . . . . . . . . . . . . . . . . . . . . . . . . . 45
Sebastian Otte, Fachhochschule Wiesbaden
Seite 56
C++ Metaprogrammierung
Literatur
Literatur
[Arc01] Arce, Tomas: Faster Vector Math Using Templates. http://www.flipcode.
com/archives/Faster_Vector_Math_Using_Templates.shtml, 2001. – besucht 29.10.2008
[C9903] C99 Committee:
C99 - Rationale for International Standard Programming Languages C. http://www.open-std.org/jtc1/sc22/wg14/www/
C99RationaleV5.10.pdf, 2003. – besucht 27.11.2008
[DAR09] Dawes, Beman ; Abrahams, David ; Rivera, Rene: Boost C++ Libraries.
http://www.boost.org, 2009. – besucht 24.01.2009
[EP08] Erk, Katrin ; Priese, Lutz: Theoretische Informatik. 3. Auflage. Berlin :
Springer Verlag, 2008
[Hof05] Hoffmann, Gerhard:
C++ Metaprogrammierung.
http://www.cdc.
informatik.tu-darmstadt.de/lehre/SS05/seminar/C++/Hoffmann_Meta.
pdf, 2005. – besucht 25.11.2008
[Lou04] Louis, Dirk: C/C++ Kompendium. Auflage 2004. München : Markt + Technik
Verlag, 2004
[Mül97] Müller, Udo: C++ Implementierungstechniken. 1. Auflage. Bonn : Thomson
Publishing, 1997
[Vel03] Veldhuizen, Todd L.: C++ Templates are Turing Complete. http:
//ubiety.uwaterloo.ca/~tveldhui/papers/2003/turing.pdf, 2003. – besucht 9.12.2008
[VJ07] Vandevoorde, David ; Josuttis, Nicolai M.: C++ Templates. 9. Auflage.
Boston : Addison-Wesley, 2007
[Wol06] Wolf, Jürgen: C++ von A bis Z. 1. Auflage. Bonn : Galileo Press, 2006
Sebastian Otte, Fachhochschule Wiesbaden
Seite 57
Herunterladen