Variadic Templates

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:

1template<class T> void use_argument(T byValue);
2template<class T> void use_argument(T& byReference);

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:

 1template<class... T> class VarTempl;
 2
 3template<> 
 4class VarTempl<>
 5{  // no args
 6};
 7
 8template<class T, class... Others>
 9class VarTempl<T, Others...> : public VarTempl<Others...>
10{  // N-args extends (N-1)-args
11};

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.