C++ Tutorial

Funktions- und Variablentemplates

Ein Template ist eine Art Vorlage für den Compiler, anhand der er Datenstrukturen und Funktionen erstellen kann. Die Programmierung mithilfe von Templates wird auch als generische Programmierung bezeichnet.

C++ kennt prinzipiell drei Arten von Templates: Funktionstemplates, Variablentemplates und Klassentemplates. In diesem Kapitel werden wir uns die ersten beiden Arten ansehen, die Funktions- und Variablentemplates.

Beginnen wir mit den Funktionstemplates. Ein Funktionstemplate ist eine Vorlage für gleichartige Funktionen, die sich in mindestens einem der folgenden Punkte unterscheiden:

  • Returntyp der Funktion
  • Datentypen der Parameter
  • Datentypen von lokalen Variablen

Sehen Sie sich einmal die drei nachfolgenden Funktionen Max() an, die alle das gleiche berechnen: Sie liefern von zwei übergebenen Werten den größeren Wert zurück. Der Unterschied liegt einzig und allein im Datentyp der Parameter.

1: short Max(const short p1, const short p2)
2: {
3:    return ((p1>p2)? p1 : p2);
4: }
5: long Max(const long p1, const long p2)
6: {
7:    return ((p1>p2)? p1 : p2);
8: }
9: float Max(const float p1, const float p2)
10: {
11:    return ((p1>p2)? p1 : p2);
12: }

Definition eines Funktionstemplates

template<> Funktionstemplates

Wandeln wir die Funktionen Max() in ein Funktionstemplate um.

1. Schritt

Im ersten Schritt werden alle Funktionen bis auf eine entfernt und die Datentypen, die von Funktion zu Funktion unterschiedlich sind, durch einen beliebigen Namen ersetzt (der kein Schlüsselwort sein darf). Dieser beliebige Name wird als formaler Datentyp bezeichnet. Im nachfolgenden Beispiel wurden die Datentypen durch den formalen Datentyp T ersetzt.

1: // Ersetzen der Datentypen
2: T Max(const T p1, const T p2)
3: {
4:    return ((p1>p2)? p1 : p2);
5: }

2. Schritt

Im zweiten Schritt müssen wir dem Compiler etwas helfen. Damit er erkennt, dass T ein Platzhalter für einen später festzulegenden Datentyp ist, wird vor die Funktion die Anweisung

template <typename T>

gesetzt.

1: // Spezifikation des formalen Datentyps
2: template <typename T>
3: T Max(const T p1, const T p2)
4: {
5:    return ((p1>p2)? p1 : p2);
6: }

Und fertig ist die Definition des Funktionstemplates. Beachten Sie, dass es sich hier um eine 'unvollständige' Definition handelt, d.h., der Compiler erzeugt noch keinen Code, da er den für den formalen Datentyp einzusetzenden Datentyp zu diesem Zeitpunkt nicht kennt.

Diese Art der Definition von Funktionstemplates war bis C++20 die einzige Möglichkeit Funktionstemplates zu definieren.

auto Funktionstemplates

Ab C++20 kann die Definition eines Funktionstemplates vereinfacht werden, indem anstelle der Datentypen das Schlüsselwort auto angegeben wird.

1: // Template-Definition mit auto
2: auto Max(auto p1, auto p2)
3: {
4:    return ((p1 > p2) ? p1 : p2);
5: }

Der Nachteil dieser Template-Definition besteht darin, dass sie nicht typsicher ist. Während bei der Definition mittels template <typename T> beide Parameter den gleichen Datentyp besitzen müssen, können bei der Definition mittels auto die Parameter unterschiedliche Datentypen besitzen. Aus diesem Grund wird im weiteren Verlauf (fast) nur die erste Variante des Funktionstemplates verwendet.

Aufruf von Funktionstemplates

Ist das Funktionstemplate definiert, kann die hierüber deklarierte Funktion wie jede normale Funktion aufgerufen werden.

1: // Templatedefinition
2: template <typename T>
3: T Max(const T p1, const T p2)
4: {
5:    return ((p1>p2)? p1 : p2);
6: }
7: // Funktionsaufrufe
8: shortMax = Max(shortV1, shortV2);
9: longMax = Max(longV1, longV2);
10: floatMax = Max(floatV1, floatV2);

Trifft der Compiler beim Übersetzen auf den Aufruf einer Funktion, führt er intern folgende Schritte durch:

Zuerst wird geprüft, ob es eine Funktion gibt bei der die Datentypen der Parameter genau zu den Argumenten beim Aufruf passen. Ist dies der Fall, wird die Funktion aufgerufen.

Gibt es keine entsprechende Funktion, wird nach einem Funktionstemplate gesucht. Gibt es ein solches, wird der formale Datentyp durch den tatsächlichen Datentyp der Argumente beim Funktionsaufruf ersetzt und die Funktion durch den Compiler generiert/instanziiert. Im Beispiel werden also drei Funktionen durch den Compiler erstellt, wobei der formale Datentyp T nacheinander durch die Datentypen short, long und float ersetzt wird.

1: // vom Compiler instanziierte Funktionen
2: short Max(const short p1, const short p2)
3: {...}
4: long Max(const long p1, const long p2)
5: {...}
6: float Max(const float p1, const float p2)
7: {...}

Gibt es weder eine Funktion noch ein Funktionstemplate, das zum Funktionsaufruf passt, wird eine Fehlermeldung ausgegeben.

Falls keine C++-Module verwendet werden, sondern getrennte Dateien für die Deklarationen der Funktionen (Header-Dateien) und deren Definitionen (Quellcode-Dateien), ist ein Funktionstemplate immer in der Header-Datei zu definieren! Die endgültige Funktion wird ja erst beim Aufruf der Funktion generiert, und dazu benötigt der Compiler den Code der Funktion.

Spezialisierung und Überladen von Funktionstemplates

Was passiert, wenn die Funktion Max() mit zwei C-Strings (char-Zeiger) aufgerufen wird, um Strings miteinander zu vergleichen?

1: // Templatedefinition
2: template <typename T>
3: T Max(const T p1, const T p2)
4: {
5:    return ((p1>p2)? p1 : p2);
6: }
7: // Aufruf der Funktion
8: const char *pName1 = "eins", *pName2 = "zwei", *pMax;
9: ....
10: pMax = Max(pName1, pName2);

Da der Compiler beim Aufruf der Funktion den formalen Datentyp durch den tatsächlichen Datentyp ersetzt, wird er die nachfolgend dargestellte Funktion generieren.

const char* Max(const char* p1, const char* p2)
{
   return ((p1>p2)? p1 : p2);
}

Doch diese Funktion vergleicht nicht die Strings, sondern deren Adressen! Was also tun? Zum einen können Templates für Datentypen spezialisiert werden. Um ein spezialisiertes Funktionstemplate zu erstellen, wird zunächst die template-Anweisung angegeben, aber ohne formalen Datentyp in spitzen Klammer. Der Datentyp, für den dieses Funktionstemplate verwendet werden soll, wird nach dem Funktionsnamen in spitzen Klammern angegeben. Ergibt sich dieser Datentyp von alleine aus den Argumenten beim Aufruf der Funktion, kann die Angabe des Datentyps entfallen.

1: // Allgemeine Templatedefinition
2: template <typename T>
3: T Max(const T p1, const T p2)
4: {
5:    return ((p1>p2)? p1 : p2);
6: }
7: // Spezialisiertes Funktionstemplate
8: template<>
9: const char* Max<const char*>(const char *p1,
10:                             const char *p2)
11: // Alternative Schreibweise
12: // template <>
13: // const char* Max(const char *p1, const char *p2)
14: {
15:    if (strcmp(p1,p2) > 0)

16:       return p1;
17:    else
18:       return p2;
19: }

Eine andere Möglichkeit, die C-Strings zu vergleichen, besteht darin, eine Funktion vorzugeben. Wie zuvor erwähnt, prüft der Compiler vor der Generierung einer Funktion aus einem Funktionstemplate zuerst, ob es eine Funktion mit den entsprechenden Datentypen der Parameter gibt. Gibt es eine solche Funktion, ruft er diese auf.

1: // Allgemeine Templatedefinition
2: template <typename T>
3: T Max(const T p1, const T p2)
4: {
5:    return ((p1>p2)? p1 : p2);
6: }
7: // Explizite Funktion zum Vergleich von C-Strings
8: const char* Max(const char* p1, const char* p2)
9: {
10:    if (strcmp(p1,p2) > 0)
11:       return p1;
12:    else
13:       return p2;
14: }

Lokale Daten mit formalem Datentyp

Der formale Datentyp kann nicht nur bei Parametern eingesetzt werden, sondern auch bei der Definition von lokalen Variablen. Bei der Generierung der Funktion durch den Compiler wird dieser formale Datentyp wieder durch den tatsächlichen Datentyp ersetzt.

1: // Funktionstemplate
2: template <typename T>
3: void Swap (T& p1, T& p2)
4: {
5:    T temp = p1;
6:    p1= p2;
7:    p2 = temp;
8: }
9: // Aufruf der Funktion
10: short var1, var2;
11: ...
12: Swap(var1, var2);

Template-Parameter

Funktionstemplates können nicht nur einen sondern auch mehrere formale Datentypen besitzen. Im nachfolgenden Beispiel wird die Funktion Func() mit zwei Argumenten mit unterschiedlichen Datentypen aufgerufen. Und damit sind bei der Spezifikation des Funktionstemplates zwei formale Datentypen anzugeben.

1: // Funktionstemplate
2: template <typename T1, typename T2>
3: void Func(T1 p1, T2 p1)
4: {
5:    T1 loc1 = p1;
6:    T2 loc2 = p2;
7:    ...
8: }
9: // Aufruf der Funktion
10: float fVar;
11: char *pChar;
12: ...
13: Func (fVar, pChar);

Neben Parametern mit formalen Datentypen können Funktionstemplates Parameter mit definierten Datentypen besitzen. Im Beispiel erhält das Funktionstemplate Func() als zweiten Parameter p2 einen int-Wert, der hier zusätzlich einen Defaultwert besitzt.

1: template <typename T>
2: void Func(T p1, int p2=10)
3: {
4:    ...
5: }

Mithilfe von mehreren formalen Datentypen und der Spezialisierung eines Templates lässt sich auch ein Problem lösen das auftritt, wenn der Returntyp des Funktionstemplates ein formaler Datentyp ist. In einem solchen Fall kann der Datentyp des Rückgabewerts eventuell nicht eindeutig aus dem Funktionsaufruf bestimmt werden. So kann im nachfolgenden Beispiel das Funktionstemplate Calculate() einen beliebigen Datentyp zurückliefern, der sich für die Zuweisung in den Datentyp der Variablen var2 konvertieren lässt.

1: template <typename T1, typename T2>
2: T1 Calulate(T2 val)
3: {...}
4: // Welchen Datentyp liefert Calculate()?
5: float var1;
6: double var2 = Calculate(var1);

Können Datentypen der Template-Parameter nicht automatisch bestimmt werden, sind sie explizit beim Aufruf der Funktion anzugeben. Dies erfolgt durch Angabe der Datentypen in spitzen Klammern nach dem Funktionsnamen. Im nachfolgenden Beispiel hat der Parameter T1 den Datentyp double und T2 den Datentyp float.

1: template <typename T1, typename T2>
2: T1 Calulate(T2 val)
3: {...}
4: // Explizite Typangabe fuer Template-Argumente
5: float var1;
6: double var2 = Calculate<double,float>(var1);

Kann einer der Datentypen implizit durch den Compiler bestimmt werden (wie im nachfolgenden Beispiel der Datentyp des Parameters T2), kann dieser beim Aufruf der Funktion weggelassen werden. Die Datentypen darauffolgenden formalen Parameter müssen sich dann aber ebenfalls implizit ergeben.

1: template <typename T1, typename T2>
2: T1 Calulate(T2 val)
3: {...}
4: // Explizite Typangabe des 1. Datentyps (Returntyp)
5: float var1;
6: double var2 = Calculate<double>(var1);

Non-type Parameter

Funktionstemplates können auch sogenannte non-type Parameter besitzen. Die Besonderheit des non-type Parameters ist, dass alle Vorkommen des Parameters bei der Instanziierung der Funktion durch das übergebene Argument ersetzt werden.

Ein non-type Parameter kann sein

  • ein bool-, Zeichen- oder Integer-Datentyp
  • Gleitkomma-Datentyp
  • Zeiger auf ein Datum oder eine Funktion
  • lvalue Referenz auf ein Objekt oder eine Funktion
  • Memberzeiger
  • std::nullptr_t (Datentyp des nullptr)
  • eine Klasse mit nur public- und non-mutable-Eigenschaften

Der non-type Parameter wird, wie die formalen Datentypen, innerhalb der template-Anweisung aufgeführt und besteht aus dem Datentyp, dem Parameternamen und einem optionalen Defaultwert.

Beim Aufruf der Funktion ist dann nach dem Funktionsnamen in spitzen Klammern zusätzlich der non-type Parameter anzugeben, wenn für ihn kein Defaultwert definiert ist.

1: // Funktionstemplate zur Reservierung und Initialisierung
2: // eines Feldes. Zurueckgegeben wird ein Zeiger auf
3: // den reservierten Speicher
4: template<typename T, int SIZE=5>
5: T* FillBuffer(T val)
6: {
7:    static T buffer[SIZE];
8:    for (auto& elem: buffer)
9:       elem = val;
10:   return buffer;
11: }
12: // Mögliche Aufrufe
13: // Reserviert ein float-Feld mit 5 Elementen und
14: // initialisert es mit dem Wert 1.2f
15: auto res1 = FillBuffer(1.2f);
16: // Reserviert ein int-Feld mit 8 Elementen und
17: // initialisiert es mit dem Wert 10
18: auto res2 = FillBuffer<int,8>(10);

Variadische Funktionstemplates

Sehen wir uns zunächst an, was variadische Funktionen sind. Variadische Funktionen sind Funktionen, welche eine variable Anzahl von Argumenten beim Aufruf besitzen.

1: int main()
2: {
3:    auto x1 = 11; // Beliebige Variable
4:    // Berechne Mittelwert 4 Werten
5:    auto av1 = Average(1.1,2.2f,3.3,x1);
6:    // Berechne Mittelwert aus 7 Werten
7:    auto av2 = Average(5,6,7,x1,77,66,x1);
8:    // Mittelwerte ausgeben
9:    std::println("{},{}", av1,av2);
10: }

Die Funktion Average() soll aus einer beliebigen Anzahl von Werten den Mittelwert berechnen. Da die Anzahl der Werte von Funktionsaufruf zu Funktionsaufruf variiert, kann hierfür keine Funktion mit einer fixen Anzahl von Parametern verwendet werden. Und solch eine Funktion mit einer variablen Anzahl von Parametern wird als variadische Funktion bezeichnet.

Hinzu kommt, dass die Datentypen der Argumente von Aufruf zu Aufruf verschiedenen sein können, ja selbst innerhalb eines Aufrufs differieren können. Daraus folgt, dass die Funktion zusätzlich als Funktionstemplate zu definieren ist.

Der Mittelwert einer Reihe von Werten ist bekannterweise die Summe der Werte dividiert durch deren Anzahl. Fangen wir mit der Berechnung der Summe aus den Werten mit unterschiedlichen Datentypen an. Um die Summe von zwei Werten mit beliebigen Datentypen zu berechnen, könnte z.B. das folgende Funktionstemplate eingesetzt werden:

1: // Summe zweier Zahlen mit beliebigem Datentyp berechnen
2: template <typename T1, typename T2>
3: auto Sum(T1 val1, T2 val2)
4: {
5:    return val1+val2;
6: }

Beachten Sie, dass der Datentyp des Returnwerts automatisch bestimmt wird. D.h., der Datentyp des zurückgelieferten Wertes richtet sich nach dem Datentyp, den die Addition zurückliefert.

Um jetzt ein Funktionstemplate zu definieren, das die Summe aus einer beliebigen Anzahl von Werten mit beliebigen Datentypen berechnet, wird als Template-Argument ein sogenanntes parameter pack angegeben. Ein parameter pack wird dadurch gekennzeichnet, dass in der template-Anweisung vor dem formalen Datentyp drei Punkte stehen.

Um einen Funktionsparameter als parameter pack zu kennzeichnen, werden ebenfalls drei Punkte angegeben, diesmal vor dem Parameternamen. In der Parameterliste eines Funktionstemplates muss ein parameter pack immer als letzter Parameter stehen.

1: // Variadisches Funktionstemplate
2: template <typename ...T>
3: auto Sum(???, T ...pack)
4: {
5:    return ???;
6: }

Sehen wir uns an, wie die vollständige Summenbildung aussieht:

#include <iostream>
// Funktionstemplate ohne parameter pack
// Liefert lediglich den uebergebenen Wert zurueck
template <typename first>
auto Sum(first val)
{
   return val;
}
// Funktionstemplate mit parameter pack
// Extrahiert ersten Wert aus uebergebenem parameter pack
template <typename first, typename ...tail>
auto Sum (first val, tail ...rest)
{
   // Addiert zum extrahierten Wert die uebrigen Werte
   // des parameter packs indem die Funktion erneut
   // aufgerufen wird (Rekursion!)
   return val + Sum(rest...);
}
// main Funktion
int main()
{
   // Summe der Werte ausgeben
   std::cout << Sum(1, 2.4, 100L);
}

In main() wird die Funktion Sum() mit drei Argumenten aufgerufen, worauf der Compiler aufgrund der Datentypen der Argumente die Funktion Sum(int,double,long) erzeugt und aufruft. Der 'Trick' dabei ist, dass der erste übergebene Wert, hier der int-Wert, dem Parameter val zugewiesen wird und die restlichen Werte dem parameter pack rest. In Sum() wird nun Sum() erneut aufgerufen, dieses Mal mit dem übrig geblieben parameter pack, d.h., es wird vom Compiler die Funktion Sum(double,long) erzeugt. Bei diesem zweiten Aufruf wird der erste Wert im parameter pack, jetzt der double-Wert, dem Parameter val zugewiesen und dann erneut Sum() mit dem reduzierten parameter pack aufgerufen. Da im parameter pack nur noch ein Wert, der long-Wert, enthalten ist, führt dies zum Aufruf des ersten Funktionstemplates. Damit ergeben sich folgende Aufrufe ({...} kennzeichnet den Inhalt des parameter pack):

Sum (1,{2.4,100L});
Sum (2.4, {100L});
Sum (100L);

Variadische Funktionstemplates sind oft rekursiv, wobei als "Abbruchkriterium" der Rekursion ein Funktionstemplate verwendet wird, welches nur einen formalen Parameter besitzt.

Da wir jetzt die Summe aus einer beliebigen Anzahl von Daten mit (fast) beliebigen Datentypen berechnen können, ist für die eigentliche Mittelwertbildung die berechnete Summe durch die Anzahl der Werte zu dividieren. Um die Anzahl der Werte in einem parameter pack zu ermitteln, wird der sizeof...(pp) Operator verwendet. Auch hier dienen wieder drei Punkte dazu, diesen Operator vom normalen sizeof Operator zu unterscheiden. Als Argument erhält sizeof...(pp) ein parameter pack. Und damit sieht das vollständige Beispiel für die Berechnung des Mittelwerts wie folgt aus:

#include <iostream>
// Funktionstemplate ohne parameter pack
// Liefert lediglich den uebergebenen Wert zurueck
template <typename first>
auto Sum(first val)
{
   return val;
}
// Funktionstemplate mit parameter pack
// Extrahiert ersten Wert aus uebergebenem parameter pack
template <typename first, typename ...tail>
auto Sum (first val, tail ...rest)
{
   // Addiert zum extrahierten Wert die uebrigen Werte
   // des parameter packs indem die Funktion erneut
   // aufgerufen wird (Rekursion!)
   return val + Sum(rest...);
}
// Funktionstemplate zur Mittelwertberechnung
// Erhält die Werte als parameter pack uebergeben
template <typename ...ppack>
auto Average(ppack ...data)
           ->decltype(Sum(data...)/sizeof...(data))
{
   // Uebergebe parameter pack komplett an Summen-Funktion
   // und dividiere die Summe durch Anzahl der Werte
   // im parameter pack
   return Sum(data...)/sizeof...(data);
}
// main Funktion
int main()
{
   // Summe der Werte ausgeben
   std::cout << Average(1,2.4,100L);
}

Externe Funktionstemplates

Wie erwähnt, werden Funktionstemplates erst dann instanziiert, wenn der Compiler auf einen entsprechenden Funktionsaufruf triff. Wird in mehreren Quellcode-Dateien ein Funktionstemplate mit den gleich Parameter-Datentypen aufgerufen, instanziiert der Compiler das Funktionstemplate zunächst mehrfach. Erst beim Zusammenbinden (Linken) der Dateien zu einem ausführbaren Programm werden diese mehrfachen Instanzen zusammengefasst, d.h. der Linker entfernt bis auf eine Instanz alle anderen. Um diese mehrfache Instanziierung von vornherein zu vermeiden, kann ein Funktionstemplate als extern deklariert werden.

1: // Datei templ.h
2: template <typename T>
3: void Swap (T v1, T2 v2)
4: {
5:    ... // Anweisungen
6: }

1: // Datei source1.cpp
2: #include "templ.h"
3: void Func1()
4: {
5:    int var1,var2;
6:    ...
7:    Swap(var1, var2); // Instanz. des Funktionstpl.
8: ...
9: }

1: // Datei source2.cpp
2: #include "templ.h"
3: extern template <typename T> Swap(T,T);
4: void Func2()
5: {
6:    int x1,x2;
7:    ...
8:    Swap(x1, x2); // Keine Instanziierung der Tpl-Funktion
9:    ...
10: }

Beim Übersetzen der Quelldatei source1.cpp wird durch den Aufruf des Funktionstemplates Swap() zunächst eine Instanz der Funktion Swap(int,int) angelegt. Übersetzt der Compiler die Quelldatei source2.cpp, wird durch die extern-Anweisung ein erneutes Instanziieren des Funktionstemplates verhindert.

Wird ein Funktionstemplate in einer Quelldatei als extern deklariert, aber vergessen das Funktionstemplate in einer anderen Quelldatei zu instanziieren, erfolgt beim Linken der Dateien eine Fehlermeldung.

Variablentemplates

Ein Variablentemplate definiert eine Variable, deren Datentyp erst beim Übersetzen des Programms festgelegt wird. Die Definition eines Variablentemplates hat folgende Syntax:

template <typename T> [QUALI] T Name [= INIT];

Die Angaben in Klammern [...] sind optional. QUALI gibt den Qualifizierer der Variablen an und kann const, volatile oder constexpr sein. Name ist der Name der Variable und INIT ein Initialisierungsausdruck. Somit erzeugt die Anweisung

template <typename T> constexpr T data = 10.0/3.0;

zunächst nur eine Vorlage für den Compiler, wie er die Variable data zu instanziieren hat. Die Anweisung definiert keine Variable (oder constexpr wie im Beispiel). Erst durch eine entsprechende Instanziierung von data wird eine Variable bzw. constexpr erzeugt.

Da der Compiler den für T einzusetzenden Datentyp nicht mehr wie bei Funktionstemplates aus dem Datentyp eines Parameters bestimmen kann, ist der Datentyp bei der Instanziierung der Variable in spitzen Klammern mit anzugeben.

auto var1 = data<double>;
auto var2 = data<int>;

Damit hat die Variable var1 den Datentyp double und den Inhalt 3.3333.., während die Variable var2 den Datentyp int und den Inhalt 3 hat.

Doch wo ist der Einsatz eines solchen Variablentemplates sinnvoll? Sehen Sie sich dazu einmal folgendes Programm und seine Ausgaben an:

#include <iostream>
// Variablentemplate
template<typename T> constexpr T tData = T(10.0/3.0);
// Gleichwertiger constexpr Ausdruck
constexpr auto cData = 10.0/3.0;
// Funktionstemplate verwendet Variablentemplate
template<typename T>
T UseTempl(T val)
{
   return val*tData<decltype(val)>;
}
// Funktionstemplate verwendet konstanten Ausdruck
template<typename T>
T UseConst(T val)
{
   return val*cData;
}
int main()
{
   std::cout << "UseTempl<int> : " << UseTempl(3) << '\n';
   std::cout << "UseConst<int> : " << UseConst(3) << '\n';
}

UseTempl : 9
UseConst : 10

Beide Funktionstemplates multiplizieren den Ausdruck 10.0/3.0 mit dem Integer-Wert 3. Jedoch wird die Berechnung im Funktionstemplate UseTempl() als Integer-Rechnung durchgeführt und im Funktionstemplate UseConst() als Gleitkomma-Rechnung. Und das, obwohl beide Funktionen den gleichen Parameter erhalten. Warum? Nun, in der Funktion UseTempl() wird eine reine Integer-Multiplikation ausgeführt, da tData hier den Datentyp des übergebenen Parameters annimmt. In der Funktion UseConst() hingegeben wird immer eine Gleitkomma-Multiplikation ausgeführt, da eine Multiplikation eines Integer-Datums mit einem Gleitkomma-Datum stets als Gleitkomma-Multiplikation durchgeführt wird.

D.h. durch Verwendung von Variablentemplates kann z.B. zur Compilezeit festgelegt werden, ob eine Integer- oder Gleitkomma-Berechnung durchgeführt wird.

Fold expression

Durch den Einsatz einer fold expression können binäre Operatoren auf eine variable Anzahl von Template-Parameter angewandt werden. Wie Sie erfahren haben, wird eine variable Anzahl von Parameter über ein parameter pack an ein Funktionstemplate übergeben. Bisher sah die Funktion zum Aufsummieren von beliebig vielen Werten so aus:

1: // Funktionstemplate ohne parameter pack
2: // Liefert lediglich den uebergebenen Wert zurueck
3: template <typename first>
4: auto Sum(first val)
5: {
6:    return val;
7: }
8: // Funktionstemplate mit parameter pack
9: // Extrahiert ersten Wert aus uebergebenem parameter pack
10: template <typename first, typename ...tail>
11: auto Sum( first val, tail ...rest)
12: {
13:    return val + Sum(rest...);
14: }

D.h., es wurde immer eine Art "Ausstiegstemplate" benötigt. Mithilfe der fold expression kann dies wie folgt vereinfacht werden:

#include <iostream>
// Funktionstemplate mit fold expression
template <typename ...ppack>
auto Sum(ppack ...list)
{
   // Addiert alle Werte des parameter packs
   return (plist + ...);
}
// main Funktion
int main()
{
   // Summe der Werte ausgeben std::cout << Sum(1,2.4,100L) << '\n';
}

Der Ausdruck (plist + ...) wird als fold expression bezeichnet und summiert die im parameter pack übergebenen Wert auf. Anstelle des Plus-Operators sind die binären Operatoren +, -, *, /, %, ^, &, |, =, <, >, <<, >>, ==, !=, <=, >=, &&, || .*, ->* sowie deren Kurzschreibweisen und der Komma-Operator zugelassen.

Damit es nicht zu trivial wird, gibt es zwei unterschiedliche fold expression:

(... OPERATOR pack) und
(pack OPERATOR ...)

Die erste fold expression wird als left fold bezeichnet und die zweite dementsprechend als right fold. Der Unterschied liegt in der Reihenfolge der Auswertung.

#include <iostream>
// Funktionstemplate mit right fold expression
template <typename ...args>
auto SubRight(args ...arguments)
{
   // Subtrahiert alle Werte des parameter packs
   return (arguments - ...);
}
// Funktionstemplate mit left fold expression
template <typename ...args>
auto SubLeft(args ...arguments)
{
   // Subtrahiert alle Werte des parameter packs
   return (... - arguments);
}
// main Funktion
int main()
{
   // Wert subtrahieren
   std::cout << SubRight(1,2,3,4) << '\n';
   std::cout << SubLeft(1,2,3,4) << '\n';
}

SubRight() führt folgende Berechnung durch:

(1-(2-(3-4))) = -2

SubLeft() dagegen führt folgende Berechnung durch:

(((1-2)-3)-4) = -8

D.h., die right fold expression baut die Klammerebenen von links nach rechts auf und die left fold expression in umgekehrter Reihenfolge. Beachten müssen Sie, dass die fold expression stets in Klammern steht. Außerdem kann der fold expression ein Initialwert mitgegeben, was hier nicht weiter betrachtet werden soll, um das Ganze nicht noch komplizierter zu machen.


Copyright 2024 © Wolfgang Schröder
E-Mail mit Fragen oder Kommentaren zu dieser Website an: info@cpp-tutor.de
Impressum & Datenschutz