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.
- Rainer Grimm
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).
Lazy- versus Eager- Template-Instanziierung
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
Eine persönliche Anmerkung:
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.
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 Funktionstd::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 vonLockable
-Typen in einem atomaren Schritt zu sperren. Einen lockable Typ in einem atomaren Schritt zu locken ist trivial. Folglich benötigtstd::lock
mindestens zwei Argumente.Lockable
wird die Anforderung genannt. Datentypen, dieLockable
unterstĂĽtzen, mĂĽssen die Memberfunktionenlock
,unlock
undtry_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.
sizeof...
-Operator
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.
Wie geht's weiter?
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. ()