Klein, aber oho: Platziert ein Entwickler drei Punkte (“…”) geschickt an der richtigen Stelle im C++-Code, entpacken die so genannten Variadic Templates ihre Argumente an Ort und Stelle.
Was haben »std::tuple()« , »std::thread()« , »std::make_unique()« und »std::make_shared()« gemeinsam? Als Variadic Templates akzeptieren sie beliebig viele Argumente, ihr Symbol ist die Ellipse. Deren Zweck ist schnell erklärt (siehe Kasten “Ellipse”). Ein Variadic Template enthält Parameter-Packs und gibt die Anzahl der darin enthaltenen Elemente zurück. Dabei tritt das Parameter-Pack wahlweise als Template-Parameter-Pack auf, das keines bis beliebig viele Template-Argumente aufnimmt, oder als Function-Parameter-Pack, das eine analoge Menge an Funktionsargumenten akzeptiert. Anschaulicher erklärt eine Anwendung die neue Begrifflichkeit.
Ellipse
Die drei Punkte »…« , auch Ellipse genannt, haben eine lange Geschichte in C und C++. Sie verwandeln gewöhnliche in spezielle Funktionen, die beliebig viele Argumente annehmen. Diese heißen variadische Funktionen (Variadic Functions) und ihr prominentester Vertreter ist wohl »printf()« .
Der entscheidende Unterschied zwischen variadischen Funktionen und Templates besteht darin, dass die Ersteren nicht typsicher sind. Beispielsweise verhält sich »printf()« unvorhersehbar, wenn sie an eine falsche Typ-Information gerät. Ein typischer Einsatzzweck variadischer Templates besteht aus diesem Grund darin, ein typsicheres »printf()« zu implementieren [1].
Der “sizeof…”-Operator
Der mit C++11 eingeführte »sizeof…« -Operator ermittelt als Variadic Template im Gegensatz zu seinem klassischen Pendant direkt die Anzahl seiner Argumente. Listing 1 zeigt ihn in Aktion.
Listing 1
count.cpp
01 #include <iostream>
02 #include <list>
03
04 template <typename ... Args>
05 int count(Args ... args){
06 return (sizeof...(args));
07 }
08
09 int main(){
10
11 std::cout << std::endl;
12
13 std::list<int> myList{1,2,3,4,5,6,7,8,9};
14
15 std::cout << "count()= " << count() << std::endl;
16 std::cout << "count(one,3.14,myList)= " << count("one", 3.14 , myList ) << std::endl;
17 std::cout << "count(myList)= " << count(myList) << std::endl;
18
19 std::cout << std::endl;
20
21 }
Bei dem »count()« -Funktionstemplate ab Zeile 4 handelt es sich um ein Variadic Template. Dank der Zeilen 15 bis 17 ruft das übersetzte Programm das Funktionstemplate mit keinem, einem oder drei Argumenten auf (Abbildung 1).
Um das zu erreichen, definiert »typename … Args« in Zeile 4 zunächst ein Template-Parameter-Pack, in der Folge kann »count()« beliebig viele Template-Argumente annehmen. Ruft das Programm ab Zeile 14 die »count()« -Funktion auf, entpackt es automatisch die Funktionsaufrufe des Function-Parameter-Pack »Args … args« aus Zeile 5. Abbildung 2 stellt schematisch dar, wie C++ diese auf die entpackten Function-Parameter-Packs abbildet.
Zwar bestimmt der »sizeof…« -Operator (Zeile 6) problemlos die Elemente des Parameter-Pack »args« , der Entwickler kann aber auch eigene Funktionstemplates einsetzen, um Parameter-Packs zu entpacken.
Unique Pointer
Wer in C++11 einen Shared Pointer (»shared_ptr« , [2]) erzeugen möchte, greift am besten zu der Fabrikfunktion »std::make_shared()« , weil das Funktionstemplate als Variadic Template beliebig viele Argumente annimmt und seine Ressource ohne Umwege direkt erzeugt. Wer in C++11 hingegen zum Funktionstemplate »std::make_unique()« greifen möchte, um einen Unique Pointer [3] zu erzeugen, sucht vergebens, denn diese Fabrikfunktion haben die C++11-Standardisierer schlicht vergessen.
Nun gibt es zwei Optionen: Die Entwickler warten, bis der Compiler den überarbeiteten C++11-Standard C++14 unterstützt und damit auch »std::make_unique()« . Oder sie implementieren »make_unique()« einfach selbst. Aus Gründen der Sportlichkeit kommt natürlich nur Option zwei in Betracht, Listing 2 weist den Weg.
Listing 2
makeUnique.cpp
01 #include <iostream>
02 #include <memory>
03
04 class Point {
05 public:
06 Point(){}
07 Point(int y) :y(y){}
08 Point(int x, int y): x(x), y(y){}
09 friend std::ostream& operator <<(std::ostream& os, Point& p) {
10 return os << "(" << p.x << "," << p.y << ")";
11 }
12 private:
13 int x= 0;
14 int y= 0;
15 };
16
17 template<typename T, typename... Args>
18 std::unique_ptr<T> make_unique(Args&&... args){
19 return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
20 }
21
22 int main(){
23
24 std::cout << std::endl;
25
26 auto myInt= make_unique<int>(2011);
27 std::cout << "*myInt: " << *myInt << std::endl;
28
29 std::cout << std::endl;
30
31 std::cout << "*make_unique<Point>(): " << *make_unique<Point>() << std::endl;
32 std::cout << "*make_unique<Point>(2011): " << *make_unique<Point>(2011) << std::endl;
33 std::cout << "*make_unique<Point> (2011,2014): " << *make_unique<Point> (2011,2014) << std::endl;
34
35 std::cout << std::endl;
36
37 }
38
In den Zeilen 17 bis 20 spiegelt sich die ganze Funktionalität. Das Funktionstemplate besitzt den Template-Parameter »typename T« sowie ein Template-Parameter-Pack: »typename… Args« . Dabei repräsentieren »T« den Typ der zu erzeugenden Ressource und »typename… Args« die Typen seiner Argumente. Dies könnten findige Beobachter auch aus dem »return« -Ausdruck »new T(std::forward<Args>(args)…)« in Zeile 19 schließen.
Das Funktionstemplate »make_unique()« nimmt in »Args&&… arg« (Zeile 18) jedes Element des Function-Parameter-Pack als Rvalue-Referenz an: Dies bewirkt, dass »makeUnique.cpp« sämtliche Argumente identisch an den Konstruktor von »T« übergibt.
Der Konstruktoraufruf »T(std::forward <Args>(args)…)« expandiert aufgrund der Ellipse schließlich das Muster »std::forward<Args>(args)…« . Das bildet den Funktionsaufruf »make_unique<Point>(2011,2014)« (Zeile 33) auf den Konstruktoraufruf »new Point(std::forward<Args>(2011),std::forward<Args>(2014))« ab.
Die Klasse »Point« besitzt drei Konstruktoren mit keinem, einem sowie zwei Argumenten (Zeilen 6 bis 8). Das Function Template »make_unique()« erzeugt die entsprechenden Objekte (ab Zeile 31). Dies betrifft auch die »int« -Variable, die Zeile 26 des Programms mit »2011« initialisiert. Die Zeilen 27 sowie 31 bis 33 dereferenzieren dann die Unique Pointer und geben sie aus. Das ermöglicht die Klasse »Point« , weil Zeile 9 den Ausgabe-Operator überlädt. Abbildung 3 zeigt, was das Programm ausgibt.
Spezialfälle
C++11 hat daneben die drei generischen Funktionen »std::any_of()« , »std::all_of()« und »std::none_of()« im Angebot. Diese Funktionstemplates benötigen einen Bereich, den zwei Iteratoren vorgeben, sowie ein Prädikat. Dabei handelt es sich um eine aufrufbare Einheit, die einen Wahrheitswert zurückgibt. Die Funktionen bestimmen dann darüber, ob irgendein Element (»std::any_of()« ), jedes Element (»std::all_of()« ) oder kein Element des Bereichs (»std::none_of« ) das Prädikat erfüllt, weitere Details stehen unter [4].
Die Algorithmen lassen sich schnell als variadische Templates implementieren. Der Artikel schaut sich exemplarisch das Template »anyOf()« in den Zeilen 3 bis 11 an. Dabei kommen zwei Funktionstemplates zum Einsatz, wobei nur das zweite in Zeile 8 ein Variadic Template ist. Es kommt immer nur dann zum Einsatz, wenn das Parameter-Pack aus mindestens einem Element besteht.
Ist dies der Fall, wendet Zeile 10 im »return« -Ausdruck das Prädikat »p« auf »a« an und verknüpft es über ein logisches Oder mit dem Rest der Argumente (»p(a) || anyOf(p,args…)« ). Da jede Iteration das jeweils erste Element des Parameter-Pack konsumiert, sinkt die Anzahl der Argumente auf 0. Ist das Parameter-Pack komplett verarbeitet, kommt als Endbedingung das erste Funktionstemplate in Zeile 3 zum Einsatz.
Zwei Punkte interessieren an dieser Verarbeitungsstrategie besonders: Erstens entspricht das Muster, eine Operation auf dem Kopf einer Liste auszuführen und die weitere Verarbeitung auf dem Schwanz der Liste zu wiederholen, dem typischen Pattern zur Listenverarbeitung in funktionalen Programmiersprachen [5]. Zweitens funktioniert der Aufruf »anyOf(p,args…)« in Zeile 10 nicht wirklich rekursiv, da er das neu aufgerufene Funktionstemplate immer um ein Argument reduziert. Das Programm ruft hier also jedes Mal ein anderes Funktionstemplate auf.
Sehr ähnlichen Strategien folgen die Funktionstemplates »allOf()« und »noneOf()« . Sie unterscheiden sich lediglich leicht in den Auswertungslogiken der »return« -Ausdrücke. So verknüpft »allOf()« in Zeile 20 den Aufruf seines Prädikats mit einem logischen Und. Das Template »noneOf()« negiert hingegen sein auszuwertendes Argument in den Zeilen 25 und 30. Die Ausgabe des Programms, die Abbildung 4 zeigt, dürfte nun keine Überraschungen mehr bergen.
It’s complicated
Wer die bisherigen Beispiele aufmerksam verfolgt hat, kann zu der Schlussfolgerung kommen, das Variadic Templates immer Funktionstemplates sind. Dies wäre aber falsch. Das bekannte Gegenbeispiel ist »std::tuple()« , ein Variadic Template und zugleich ein Klassentemplate. Dabei folgt die Definition von »std::tuple()« dem funktionalen Muster, nach dem eine Liste sich sukzessive selbst konsumiert, auf besonders innovative Weise. Der Code in Listing 4 stellt die Strategie von »std::tuple()« auf einem stark vereinfachten Tuple vor.
Listing 4
std::tuple()
01 template <class... Ts> struct tuple {};
02
03 template <class T, class... Ts>
04 struct tuple<T, Ts...> : tuple<Ts...> {
05 tuple(T t, Ts... ts) : tuple<Ts...>(ts...), tail(t) {}
06 T tail;
07 };
Augenscheinlich besteht das Listing aus zwei Klassentemplates. Das erste, allgemeine in Zeile 1 nimmt jedes beliebige Parameter-Pack an. Das Klassentemplate der Zeilen 3 bis 8 ist hingegen teilweise spezialisiert. Das heißt, es benötigt mindestens ein Argument, damit C++ es instanziert (Zeile 3). Zusätzlich leitet der Code das Tuple mit n+1 Argumenten von dem »tuple« mit n Argumenten ab (Zeile 4).
Schwer verdaulich zeigt sich auch der Konstruktor der Klasse, der Ausdruck »tuple(T t, Ts… ts) : tuple<Ts…>(ts…), tail(t) {}« . In ihm gibt der Entwickler das erste Argument »T t« explizit an und setzt es in der Initialisierungsliste des Konstruktors ein, um das Klassenmitglied »tail(t)« zu initialisieren.
Die restlichen Argumente des Parameter-Pack dienen dazu, die Konstruktoren aller Basisklassen, von denen sich »tuple<T,Ts…>« ableitet, sukzessive mit den richtigen Argumenten zu versorgen.
Listing 3
anyAllNoneOf.cpp
01 #include <iostream>
02
03 template<typename UnaryPredicate, typename T>
04 bool anyOf(UnaryPredicate p, T a) {
05 return p(a);
06 }
07
08 template<typename UnaryPredicate, typename T, typename... Args>
09 bool anyOf(UnaryPredicate p, T a , Args... args) {
10 return p(a) || anyOf(p,args...);
11 }
12
13 template<typename UnaryPredicate, typename T>
14 bool allOf(UnaryPredicate p, T a) {
15 return p(a);
16 }
17
18 template<typename UnaryPredicate, typename T, typename... Args>
19 bool allOf(UnaryPredicate p, T a , Args... args) {
20 return p(a) && allOf(p,args...);
21 }
22
23 template<typename UnaryPredicate, typename T>
24 bool noneOf(UnaryPredicate p, T a) {
25 return not(p(a));
26 }
27
28 template<typename UnaryPredicate, typename T, typename... Args>
29 bool noneOf(UnaryPredicate p, T a , Args... args) {
30 return not(p(a)) && noneOf(p,args...);
31 }
32
33 int main(){
34
35 std::cout << std::boolalpha << std::endl;
36
37 std::cout << "anyOf([](int x){return x < 0;},1,2,3,4,5)= "
38 << anyOf([](int x){return x < 0;},1,2,3,4,5)
39 << std::endl;
40 std::cout << "anyOf([](int x){return x%2 == 0;},1,2,3,4,5)= "
41 << anyOf([](int x){return x%2 == 0;},1,2,3,4,5)
42 << std::endl;
43 std::cout << "anyOf([](bool b){return b == true;},true,false,true)= "
44 << anyOf([](bool b){return b == true;},true,false,true)
45 << std::endl;
46
47 std::cout << std::endl;
48
49 std::cout << "allOf([](int x){return x < 0;},1,2,3,4,5)= "
50 << allOf([](int x){return x < 0;},1,2,3,4,5)
51 << std::endl;
52 std::cout << "allOf([](int x){return x%2 == 0;},1,2,3,4,5)= "
53 << allOf([](int x){return x%2 == 0;},1,2,3,4,5)
54 << std::endl;
55 std::cout << "allOf([](bool b){return b == true ;},true,false,true)= "
56 << allOf([](bool b){return b == true ;},true,false,true)
57 << std::endl;
58
59 std::cout << std::endl;
60
61 std::cout << "noneOf([](int x){return x < 0;},1,2,3,4,5)= "
62 << noneOf([](int x){return x < 0;},1,2,3,4,5)
63 << std::endl;
64 std::cout << "noneOf([](int x){return x%2 == 0;},1,2,3,4,5)= "
65 << noneOf([](int x){return x%2 == 0;},1,2,3,4,5)
66 << std::endl;
67 std::cout << "noneOf([](bool b){return b == true ;},true, false, true)= "
68 << noneOf([](bool b){return b == true ;},true,false,true)
69 << std::endl;
70
71 std::cout << std::endl;
72
73 };
Leichtes Verwirrungspotenzial birgt schließlich noch, dass das erste der Template-Argumente »T« dazu verwendet, das Klassenmitglied »tail()« zu initialisieren. Denn »T« ist doch der »head« (Kopf) der Template-Argumente, der im Template zum »tail« (Schwanz) wird. Das rührt daher, dass sich die Vererbungshierarchie von hinten her aufbaut (»tuple<Ts…>(ts…), tail(t)« ).
Infos
- Typsicheres »printf()« : http://en.wikipedia.org/wiki/Variadic_template
- Rainer Grimm, “Klug aufgeräumt”: Linux- Magazin 04/13, S. 104
- Rainer Grimm, “Intelligent aufgeräumt”: Linux-Magazin 02/13, S. 90
- Generische Funktionen »std::any_of()« , »std::all_of()« und »std::none_of()« : http://en.cppreference.com/w/cpp/algorithm/all_any_none_of
- Rainer Grimm, “Funktionale Programmierung (1)”: https://www.linux-magazin.de/Online-Artikel/Funktionale-Programmierung-1-Grundzuege










