Listing 4
Die Map-Funktion zur Compilezeit
01 #include <iostream>
02 #include <tuple>
03
04 // Int2Type
05
06 template <int v>
07 struct Int2Type {
08 const static int value= v;
09 };
10
11 // a few class templates
12
13 struct ApplyId{
14 template<typename T>
15 struct apply{
16 typedef Int2Type<T::value> type;
17 };
18 };
19
20 struct MakeTen{
21 template<typename T>
22 struct apply{
23 typedef Int2Type<10> type;
24 };
25 };
26
27 struct DoubleMe{
28 template<typename T>
29 struct apply{
30 typedef Int2Type<2*(T::value)> type;
31 };
32 };
33
34 struct AbsMe{
35 template<typename T>
36 struct apply{
37 typedef Int2Type< (T::value > 0)? T::value : -(T::value) > type;
38 };
39 };
40
41 // helper function for output
42
43 template< typename head >
44 void showMe( std::tuple< head> ){
45 std::cout << head::value << "\n";
46 };
47
48 template< typename head, typename ... tail>
49 void showMe( std::tuple< head, tail ... > ){
50 std::cout << head::value << " ," ;
51 showMe( std::tuple< tail ...>() );
52 }
53
54 // map function
55
56 template<typename F, typename ... Elements> struct map;
57
58 template< typename F, typename ... Elements>
59 struct map<F,std::tuple<Elements ...> >{
60 typedef std::tuple<typename F::template apply<Elements>::type ... > type;
61 };
62
63 int main(){
64
65 std::cout << "original tupel: " << std::endl;
66 typedef std::tuple<Int2Type<-5>,Int2Type<-4>,Int2Type<-3>,Int2Type<-2>,Int2Type<-1>,Int2Type<0>,
67 Int2Type<1>,Int2Type<2>,Int2Type<3>,Int2Type<4>,Int2Type<5> >constNumbers;
68 showMe( constNumbers() );
69
70 std::cout << "\napply identity: " << std::endl;
71 typedef map<ApplyId, constNumbers >::type idConstNumbers;
72 showMe( idConstNumbers() );
73
74 std::cout << "\nset each value to 10" << std::e ndl;
75 typedef map<MakeTen, constNumbers >::type makeTenConstNumbers;
76 showMe( makeTenConstNumbers() );
77
78 std::cout << "\ndouble each value" << std::endl;
79 typedef map<DoubleMe, constNumbers >::type doubleMeConstNumbers;
80 showMe( doubleMeConstNumbers() );
81
82 std::cout << "\nabsolute value" << std::endl;
83 typedef map<AbsMe, constNumbers >::type absMeConstNumbers;
84 showMe( absMeConstNumbers() );
85
86 };
Vor der Theorie kommt die Praxis. Kompiliert und ausgeführt, ergibt der Quelltext die Ausgabe in Abbildung 4. Das Programm besteht aus der »map« -Metafunktion (Zeile 56), die beliebig viele Argumente annimmt, sie in ein »std::tuple« verpackt und die Metafunktionen »ApplyId« , »MakeTen« , »DoubleMe« und »AbsMe« auf ihre einzelnen Argumente (Zeile 60) anwendet. Das einfache Funktionstemplate »showMe« gibt den Container zu Laufzeit aus.

Abbildung 4: Ganz funktionale Schule: Die in der Templatesprache umgesetzte Map-Funktion wendet eine Funktion auf jedes Element einer Liste an.
Eindeutig funktional
Es zeigt sich: Das Programm hat alles zu bieten, was die funktionale Programmierung auszeichnet (siehe den Artikel “Funktionale Grundzüge” auf Linux-Magazin Online [4].)
Metafunktionen sind:
- Funktionen erster Ordnung, denn als Klassentemplates können Aliase (Zeile 71) auf sie erzeugt und im weiteren Programmverlauf verwendet werden.
- Funktionen höherer Ordnung, denn sie lassen sich als Argument (Zeile 56) oder Returnwert einer Metafunktion verwenden.
- Reine Funktionen, denn es gibt zur Compilezeit keinen veränderlichen Zustand.
Die Kontrollstruktur der funktionalen Programmierung ist die Rekursion über eine Liste. Dabei wird das erste Element verarbeitet und über den Rest weiter iteriert. In der funktionalen Programmierwelt haben sich »head« für den Beginn der Liste und »tail« für deren kompletten Rest etabliert. Diesem Muster folgt »showMe« ab Zeile 49.
Die Struktur »struct Int2Type« in Zeile 7 nimmt eine natürliche Zahl an und verpackt diese in einem Typ. Über »const static int value= v« in Zeile 8 stellt der Typ seinen Wert wieder als integrale Konstante zur Verfügung. Was wie obfuscated C++-Code wirkt, hat einen einfachen Grund: Um das Alias »constNumbers« in Zeile 67 zu definieren, werden Typen benötigt. Der Kunstgriff, eine natürliche Zahl in einem Typ zu verpacken, geht auf Andrei Alexandrescu [5] zurück.
Neben Alexandrescu, der die Mächtigkeit der Template-Metaprogrammierung auf einzigartige Weise in seinem Werk “Modern C++ Design” [6] angewendet hat, sind insbesondere Krzysztof Czarnecki und Ulrich W. Eisenecker zu nennen, die wegen ihrer Veröffentlichung “Generative Programming” [7] als Pioniere dieser neuen Programmiertechnik gelten.
Ungewohnt und neu sind auch die oft verwendeten drei Punkte (Ellipse), die sowohl in »showMe« als auch in »map« auftauchen. Dies sind die so genannten Variadic Templates [3]. Sie bezeichnen Templates, die beliebig viele Argumente annehmen dürfen. Stehen die drei Punkte links vom Bezeichner, werden die Argumente gepackt, rechts entpackt.
Der wohl befremdlichste Ausdruck ist »std::tuple<typename F::template apply <Elements>::type … > type« in Zeile 60. Durch das Entfernen der Schlüsselwörter, die dem C++-Parser bei der Evaluierung des Ausdrucks helfen, wird dieser schon deutlich lesbarer: »std::tuple <F::apply<Elements>::type … > type« . Die Metafunktion »F« und besonders deren Klassentemplate »apply« werden auf jeden Typ von »std::tuple« angewandt. Das Ergebnis ist, dass für jeden Typ »Int2Type« ein neuer Typ erklärt wird.
Im Falle der Metafunktion »DoubleMe« besitzt dieser neue Typ die Eigenschaft, dass seine integrale Konstante verdoppelt ist »typedef Int2Type<2*(T::value)>« . Um das Ergebnis mit »showMe( doubleMeConstNumbers() )« auszugeben, muss der neue Typ instanziiert werden. Dies geschieht zur Laufzeit.





