Variadic Templates
« | 14 Feb 2021 | »Zu Variadic Templates habe
ich historisch bedingt ein schlechtes Verhältnis. Da sie erst mit
C++11 viel zu spät in den Standard
kamen, wucherten Schnellschüsse in boost
und Co schon Jahre zuvor, die aus Code “Kot” machten.
Wie auch immer, heute, mit dem C++20 Standard kommt man an diesem Feature eigentlich nicht mehr vorbei, wenn man die STL verstehen möchte.
Überladungen sind zu wenig
Variadic Templates sollen in C++ das ermöglichen, was variadic functions
in
C schon 30 Jahre
früher konnten: Man kann eine freie Anzahl an Argumenten an eine Funktion
übergeben und diese iteriert sie durch.
Bestes Beispiel ist prinf(format, ...)
wo man so viele Variablen übergeben
kann, wie im format
String angegeben sind.
Aber C++ unterstützte von Anfang an Funktions-Überladung und so konnte man für jede Anzahl an Argumenten eine Funktion schreiben, die sich genau um die Anzahl kümmerte und alle durften den gleichen Namen haben.
Genau so wurden viele Features von 1998 bis 2011 implementiert … doch die Sache hatte einen Haken: Es fehlten R-Value-References.
Und wenn man eine Variable “frei” übergeben möchte, soll die Funktion diese auch “ohne Auflagen” entgegennehmen.
Jedoch musst man sich entweder für “by-value” oder “by-reference” entscheiden:
Der erste Fall kopiert die Variable, erzwingt also die Kopierbarkeit des
Objektes und entkoppelt es vom Original.
Der zweite Fall bekommt mit Konstanten Probleme, man bräuchte noch eine
dritte Variante mit T const&
die dann aber in manchen Fällen T&
schlucken
kann.
Und wenn man 2, 3 oder mehr Argumente hat, müsste man alle Kombinationen von
Referenzen und Werten ausimplementieren, also bei 2 Argumenten sind das 4,
bei 3 schon 8 … allgemein also 2 hoch n
.
1template<class T> void use_argument(T&& anything);
hingegen löst alles.
Anwendung
Mein erstes Demo-Beispiel von vor langer Zeit sah etwa so aus:
1template<class T> void print1(T const& t) 2{ 3 std::cout << t; 4} 5 6void print1(nullptr_t const& n) 7{ 8 return; 9} 10 11template<class A = nullptr_t, class B = nullptr_t, 12 class C = nullptr_t, class D = nullptr_t, class E = nullptr_t> 13void print(A&& a = A(), B&& b = B(), C&& c = C(), D&& d = D(), E&& e = E()) 14{ 15 print1(a); 16 print1(b); 17 print1(c); 18 print1(d); 19 print1(e); 20} 21 22template<typename... T> void print_variadic(T... var) 23{ 24 print(var...); 25} 26 27int main() 28{ 29 print_variadic(1, 2, "Hello World"); 30 return 0; 31}
OK … es war “recht dumm” und wandelt den freien Aufruf wieder in eine feste
Anzahl von Argumenten um, bei denen die nicht benutzten durch nullptr_t
ersetzt werden, die dann keine Aktion bewirken … aber so kann man schön
zeigen, dass damit in erster Linie Funktionsaufrufe generiert werden, die nur
das weitergeben, was am Anfang durch Beistriche getrennt aufgezählt wird.
Das kann man auch gut mit einem überladenen operator,()
handhaben.
Rekursionen bevorzugt
Um das Feature im “modernen C++ Style” zu nutzen sind aber Rekursionen und rekursive Vererbungen samt Spezialisierungen angesagt:
Das klassische Beispiel dafür ist std::tuple
, doch man stellt leider auch
fest, dass das Ganze nicht mehr ganz so unkompliziert ist, wie in
C mit va_start
, va_arg
und va_end
.
Fazit
Im GATE Projekt wird diese Technik vorläufig nicht eingesetzt um mit älteren
Compilern kompatibel zu bleiben … aber … so wie auch R-Value-References
optional aktiviert werden können, denke ich über einen “schöneren”
Ersatz für die bestehenden Delegate0
, Delegate1
bis DelegateN
Klassen
nach.
Und dafür wären Variadic Templates natürlich perfekt.
Mal sehen, wann ich dazu komme.