Mehr über Variadic Templates ...

Modernes C++ Rainer Grimm  –  0 Kommentare

Es steckt eine Menge Power in den eigentümlich aussehenden drei Punkten, die in der Standard Template Library häufig verwendet werden. Aufschluss gibt eine Visualisierung der Pack-Expansion inklusive einiger Anwendungsfälle.

Mehr über Variadic Templates ...

Beginnen möchte ich diesen Post mit einer Analyse der Pack Expansion in Variadic Templates.

Pack-Expansion

Hier ist eine kleine Erinnerung: Ein variadisches Template kann eine beliebige Anzahl von Template-Parametern besitzen.

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

Dank der Ellipse (...) wird Args bzw. args zu einem sogenannten Parameter-Pack. Genauer gesagt ist Args ein Template-Parameterpaket und args ein Funktionsparameterpaket. Mit Parameter-Packs sind zwei Operationen möglich. Sie können gepackt und entpackt werden. Wenn die Ellipse links von Args ist, wird das Parameter-Pack gepackt, wenn sie rechts von Args ist, wird es entpackt. Aufgrund der Function Template Argument Deduction kann der Compiler die Template-Argumente automatisch ableiten.

Bevor ich über die Pack-Erweiterung schreibe, muss ich einen kurzen Disclaimer machen. Normalerweise wendet man die Pack Expansion nicht direkt an, sondern verwendet Variadic Templates, die das automatisch erledigen, oder Fold Expressions beziehungsweise constexpr if in C++17. Ich werde über Fold Expressions und constexpr if in zukünftigen Beiträgen schreiben. Die Visualisierung von Pack-Expansionen ist sehr hilfreich für ein besseres Verständnis von Variadic Templates und Fold Expressions.

Die Verwendung von Parameterpacks folgt einem typischen Muster.

  • Führe eine Operation auf dem ersten Element des Parameter-Pack aus und rufe die Operation rekursiv auf den restlichen Elementen auf. Dieser Schritt reduziert das Parameter-Pack sukzessive um sein erstes Element.
  • Die Rekursion endet nach einer endlichen Anzahl von Schritten.
  • Die Randbedingung ist typischerweise ein vollständig spezialisiertes Template.

Dank dieses funktionalen Musters für die Verarbeitung von Listen, lässt sich das Produkt von Zahlen zur Compile-Zeit berechnen. In Lisp wird der Kopf der Liste car und den Rest cdr genannt. In Haskell haben sich die Namen head und tail etabliert.

// multVariadicTemplates.cpp

#include <iostream>

template<int ...> // (1)
struct Mult;

template<> // (2)
struct Mult<> {
static const int value = 1;
};

template<int i, int ... tail> // (3)
struct Mult<i, tail ...> {
static const int value = i * Mult<tail ...>::value;
};

int main(){

std::cout << '\n';

std::cout << "Mult<10>::value: " << Mult<10>::value << '\n'; // (4)
std::cout << "Mult<10,10,10>::value: " << Mult<10, 10, 10>::value
<< '\n';
std::cout << "Mult<1,2,3,4,5>::value: " << Mult<1, 2, 3, 4, 5>::value
<< '\n';

std::cout << '\n';

}

Das Klassen-Template Mult besteht aus einem primären Template und zwei Spezialisierungen. Da das primäre Template (1) nicht benötigt wird, genügt in diesem Fall eine Deklaration: template<int ...> struct Mult. Die Spezialisierungen des Klassen-Template gibt es für kein Element (2) und für mindestens ein Element (3). Beim Aufruf von Mult<10,10,10>::value wird das Template mit mindestens einem Element verwendet, indem das erste Element mit dem Rest des Parameterpakets nacheinander aufgerufen wird, sodass value zum Produkt 10*10*10 expandiert. In der letzten Rekursion enthält das Parameterpaket keine Elemente und die Randbedingung tritt in Aktion: template<> struct Mult<> (1). Dies liefert das Ergebnis von Mult<10,10,10>::value= 10*10*10*1 zur Compile-Zeit.

Mehr über Variadic Templates ...

Nun zum interessanten Teil: Was passiert unter der Haube. Schauen wir uns den Aufruf Mult<10,10,10>::value genauer an. Dieser Aufruf löst keine Rekursion aus, sondern eine rekursive Instanziierung. Hier sind die wesentlichen Teile aus C++ Insights:

Mehr über Variadic Templates ...

Der Compiler erzeugt vollständige Spezialisierungen für drei (Mult<10, 10, 10>) und zwei Argumente (Mult<10, 10>). Das wirft die Frage auf: Wo sind die Instanziierungen für ein Argument (Mult<10>) und kein Argument (Mult<>)? Mult<10> wurde bereits in (4) gefordert und Mult<> (1) ist die Randbedingung.

Dazu eine kleine Anekdote: Wenn ich Variadic Templates in einer Schulung einführe, frage ich meine Teilnehmer gerne: Wer von Ihnen hat schon mal eine Ellipse benutzt? Die Hälfte meiner Teilnehmer antwortet: noch nie. Ich antworte ihnen, dass ich ihnen das nicht glaube und sie vielleicht schon von der printf-Familie gehört haben.

Eine typsichere printf-Funktion

Wohl jeder kennt die C-Funktion printf: int printf( const char* format, ... );. printf ist eine Funktion, die eine beliebige Anzahl von Argumenten erhalten kann. Ihre Mächtigkeit basiert auf dem Makro va_arg und ist daher nicht typsicher. Lasse mich eine vereinfachte printf Funktion mit Variadic Templates implementieren. Diese Funktion ist das Hello-World der Variadic Templates.

// myPrintf.cpp

#include <iostream>

void myPrintf(const char* format){ // (3)
std::cout << format;
}

template<typename T, typename ... Args>
void myPrintf(const char* format, T value, Args ... args){ // (4)
for ( ; *format != '\0'; format++ ) { // (5)
if ( *format == '%' ) { // (6)
std::cout << value;
myPrintf(format + 1, args ... ); // (7)
return;
}
std::cout << *format; // (8)
}
}

int main(){

myPrintf("\n"); // (1)

myPrintf("% world% %\n", "Hello", '!', 2011); // (2)

myPrintf("\n");

}

Wie ist der Kontrollfluss des Codes? Wenn myPrintf ohne Formatstring aufgerufen wird (1), wird (3) in diesem Fall verwendet. (2) verwendet das Funktions-Template. Das Funktions-Template prozessiert die Schleife (5) solange, wie das Formatsymbol nicht gleich `\0` ist. Wenn das Formatsymbol ungleich `\0` ist, sind zwei Kontrollflüsse möglich. Wenn das Format mit '%' beginnt (6), wird der erste Argumentwert angezeigt und myPrintf wird erneut aufgerufen, aber diesmal mit einem neuen Formatsymbol und einem Argument weniger (7). Wenn dagergen der Formatstring nicht mit '%' beginnt, wird nur das Formatsymbol angezeigt (Zeile 8). Die Funktion myPrintf (3) ist die Randbedingung für die rekursiven Aufrufe.

Die Ausgabe des Programms ist erwartungsgemäß.

Mehr über Variadic Templates ...

Wie zuvor hilft C++ Insights sehr, um einen tieferen Einblick in den Template-Instanzierungsprozess zu erhalten. Hier sind die drei Instanziierungen, die durch myPrintf("% world% %\n", "Hello", '!', 2011); verursacht werden:

  • Vier Argumente:
Mehr über Variadic Templates ...
  • Drei Argumente:
Mehr über Variadic Templates ...
  • Zwei Argumente:
Mehr über Variadic Templates ...

Kurze zweiwöchige Pause

Aufgrund meines Urlaubs und wahrscheinlich eingeschränkter Konnektivität, werde ich in den nächsten zwei Wochen keinen Beitrag veröffentlichen. Wer einen Gastbeitrag veröffentlichen möchte, kann mich gerne kontaktieren.

Wie geht's weiter?

In meinem nächsten Beitrag verwende ich Variadic Templates, um das C++-Idiom für eine vollständig generische Fabrik zu implementieren. Eine Implementierung dieses lebensrettenden C++ Idioms ist std::make_unique.