Variadic Templates oder die Power der drei Punkte

Ein Variadic Template kann eine beliebige Anzahl von Template-Parametern besitzen. Dieses Feature mag auf den ersten Blick magisch erscheinen. Daher ist es Zeit, Variadic Templates zu entmystifizieren.

In Pocket speichern vorlesen Druckansicht 146 Kommentare lesen
Lesezeit: 6 Min.
Von
  • Rainer Grimm
Inhaltsverzeichnis

Ein Variadic Template kann eine beliebige Anzahl von Template-Parametern besitzen. Dieses Feature mag auf den ersten Blick magisch erscheinen. Daher ist es Zeit, Variadic Templates zu entmystifizieren.

Manch einer mag sich wundern, dass meine Grafik mit den vorgestellten Themen die Template-Instanziierung enthält. Der Grund ist einfach: Nach meinem letzten Artikel über "Template-Instanziierung" hat einer meiner deutschsprachigen Leser (Pseudonym Urfahraner Auge) in einem Kommentar geschrieben, dass es einen wichtigen Unterschied zwischen impliziter und expliziter Instanziierung eines Templates gebe, den ich vergessen habe vorzustellen. Er hat recht. Die implizite Instanziierung von Templates ist lazy (faul), aber die explizite Instanziierung von Templates ist eager (gierig).

Die Template-Instanziierung ist lazy. Das heißt, dass eine nicht benötigte Memberfunktion eines Klassen-Templates nicht instanziiert wird. Nur die Deklaration der Member-Funktion ist verfügbar, aber nicht ihre Definition. Damit ist es möglich, ungültigen Code in einer Menber-Funktion zu verwenden, solange sie nicht aufgerufen wird.

// numberImplicitExplicit.cpp

#include <cmath>
#include <string>

template <typename T>
struct Number {
int absValue() {
return std::abs(val);
}
T val{};
};

// template class Number<std::string>; // (2)
// template int Number<std::string>::absValue(); // (3)

int main() {

Number<std::string> numb;
// numb.absValue(); // (1)

}

Wer die Member-Funktion numb.absValue() (1) aufruft, bekommst erwartungsgemäß eine Fehlermeldung zur Kompilierzeit, die besagt, dass es keine Überladung std::abs für std::string gibt. Hier sind die ersten beiden Zeilen der ausführlichen Fehlermeldung:


Nun möchte ich die Template-Instanziierung genauer erklären: Die implizite Instanziierung von Templates ist lazy, aber die explizite Instanziierung von Templates ist eager.

Wer (2) aktiviert (template class number<std::string>) und damit explizit das Klassen-Template Number instanziiert oder (3) aktiviert (template int Number<std::string>::absValue()) und damit explizit die Member-Funktion absValue für std::string instanziiert, bekommt einen Kompilierzeitfehler. Dieser ist äquivalent zum Compiler-Fehler beim Aufruf der Member-Funktion absValue in (1) (numb.absValue()). Hier sind noch einmal die ersten beiden Zeilen der Fehlermeldungen nach dem Aktivieren von (2) oder (3).

  • (2) aktiviert
  • (3) aktiviert

Ich freue mich sehr über Kommentare zu meinen Beiträgen. Sie helfen mir, über die Inhalte zu schreiben, die die Leser meines Blogs interessieren. Vor allem die deutsche Community ist sehr engagiert.

Nun aber endlich zu etwas ganz anderem: Variadic Templates.

Ein Variadic Template ist ein Template, das eine beliebige Anzahl von Template-Parametern besitzen kann. Dieses Feature mag beim ersten Mal magisch erscheinen.

template <typename ... Args>
void variadicTemplate(Args ... args) {
. . . . // four dots
}

Durch die Ellipse (...) werden Args beziehungsweise args zu einem sogenannten Parameterpack. Genauer gesagt ist Args ein Template-Parameterpack und args ein Funktionsparameterpack. Mit Ersterem sind zwei Operationen möglich: Es kann gepackt und entpackt werden. Wenn die Ellipse links von Args steht, wird das Parameterpack gepackt, wenn sie rechts steht, wird es entpackt. Durch Function Template Argument Deduction kann der Compiler die Template Argumente automatisch ableiten.

Variadic Templates werden oft in der Standard Template Library und auch in der Kernsprache verwendet.

template <typename... Types>                                        // (1)
class tuple;

template <typename Callabe, typename... Args > // (2)
explicit thread(Callable&& f, Args&&... args);

template <typename Lockable1, typename Lockable2,
typename... LockableN> // (3)
void lock( Lockable1& lock1, Lockable2& lock2, LockableN&... lockn );

sizeof...(ParameterPack); // (4)

Alle vier Beispiele aus dem C++11 Standard verwenden Variadic Templates. Die ersten drei sind Teil der Standard Template Library. Was lässt sich aus den Deklarationen bestimmen?

  • std::tuple akzeptiert eine beliebige Anzahl von verschiedenen Typen.
  • std::thread erlaubt es, ein Callable mit einer beliebigen Anzahl von Argumenten aufzurufen. Die Argumente können verschiedene Typen besitzen. Ein Callable ist eine Entität, das man aufrufen kann, beispielsweise eine Funktion, ein Funktionsobjekt oder ein Lambda-Ausdruck. Die Funktion std::thread nimmt ihr Callable und ihre Argumente per Universeller Referenz an. Wenn du mehr Details brauchst: Über die Ableitung von Template-Argumenten und Universeller Referenzen habe ich bereits in meinem Beitrag "Template Arguments" geschrieben.
  • std::lock erlaubt es, eine beliebige Anzahl von Lockable-Typen in einem atomaren Schritt zu sperren. Einen lockable Typ in einem atomaren Schritt zu locken ist trivial. Folglich benötigt std::lock mindestens zwei Argumente. Lockable wird die Anforderung genannt. Datentypen, die Lockable unterstützen, müssen die Memberfunktionen lock, unlock und try_lock anbieten.
  • Der sizeof ... - Operator gibt die Anzahl der Elemente im Parameterpack zurück.

Der sizeof...-Operator ist besonders, da hier Parameterpacks auf der C++ Kernsprache zum Einsatz kommen.

Mithilfe des sizeof...-Operators kann direkt ermittelt werden, wie viele Elemente ein Parameterpack enthält. Die Elemente werden dabei nicht ausgewertet.

// printSize.cpp

#include <iostream>

using namespace std::literals;

template <typename ... Args>
void printSize(Args&& ... args){
std::cout << sizeof...(Args) << ' '; // (1)
std::cout << sizeof...(args) << '\n'; // (2)
}

int main() {

std::cout << '\n';

printSize(); // (3)
printSize("C string", "C++ string"s, 2011, true); // (4)

std::cout << '\n';

}

Der sizeof...-Operator erlaubt es, die Größe des Template-Parameterpacks (1) und des Funktionsparameterpacks (2) zur Kompilierzeit zu bestimmen. Ich wende ihn auf ein leeres Parameterpaket (3) und ein Parameterpaket mit vier Elementen an. Das erste Element ist ein C-String und das zweite ein C++-String. Um das C++-String-Literal zu verwenden, muss ich den Namensraum std::literals (5) einbinden. C++14 unterstützt C++-String-Literale.

In meinem nächsten Beitrag tauche ich tiefer in Variadic Templates ein und stelle das funktionale Muster zur Auswertung eines Variadic Template vor. Außerdem präsentiere ich die perfekte Fabrikfunktion und springe von C++11 sechs Jahre weiter: Fold Expression in C++17. ()