Funktions- und Variablentemplates
Ein Template ist eine Vorlage für den Compiler, anhand deren 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.
short Max(const short p1, const short p2)
{
return ((p1>p2)? p1 : p2);
}
long Max(const long p1, const long p2)
{
return ((p1>p2)? p1 : p2);
}
float Max(const float p1, const float p2)
{
return ((p1>p2)? p1 : p2);
}
Definition eines 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.
// Ersetzen der Datentypen
T Max(const T p1, const T p2)
{
return ((p1>p2)? p1 : p2);
}
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.
// Spezifikation des formalen Datentyps
template <typename T>
T Max(const T p1, const T p2)
{
return ((p1>p2)? p1 : p2);
}
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.
In älteren Programmen finden Sie vielleicht noch folgende Templatedefinition:
template <class T>
T Func(T p1)
{...}
Hier wird anstelle des Schlüsselworts typename class verwendet. Dies ist aber eine veraltete Schreibweise!
auto-Funktionstemplates
Ab C++20 kann die Definition eines Funktionstemplates vereinfacht werden, indem anstelle der Datentypen das Schlüsselwort auto angegeben wird.
// Template-Definition mit auto
auto Max(auto p1, auto p2)
{
return ((p1 > p2) ? p1 : p2);
}
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.
#include <print>
// Templatedefinition
template <typename T>
T Max(const T p1, const T p2)
{
return ((p1>p2)? p1 : p2);
}
int main()
{
// 2 ints definieren
int ivar1 = 10, ivar2 = -5;
// 2 floats definieren
float fvar1 = 3.14f, fvar2 = 99.f;
// Funktionsaufrufe
std::println("Die groessere Zahl von {} und {} ist {}",
ivar1,ivar2,Max(ivar1,ivar2));
std::println("Die groessere Zahl von {} und {} ist {}",
fvar1,fvar2,Max(fvar1,fvar2));
}
Die groessere Zahl von 10 und -5 ist 10
Die groessere Zahl von 3.14 und 99 ist 99
Hier passiert nun folgendes: Trifft der Compiler beim Übersetzen auf den Aufruf einer Funktion, führt er intern folgende Schritte durch:
1. Schritt: 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.
2. Schritt: 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 zwei Funktionen durch den Compiler erstellt, wobei der formale Datentyp T durch die Datentypen int und float ersetzt wird.
// vom Compiler instanziierte Funktionen
int Max(const int p1, const int p2)
{...}
float Max(const float p1, const float p2)
{...}
3. Schritt: Gibt es weder eine Funktion noch ein Funktionstemplate, das zum Funktionsaufruf passt, wird eine Fehlermeldung ausgegeben.
Und nochmals: Bei der Auflösung eines Templates wird nie eine automatische Typkonvertierung vorgenommen. Wird z.B. versucht, für den ersten Parameter der Funktion Max() einen short-Wert anzugeben und für den zweiten Parameter einen char-Wert, müsste der Compiler den formalen Datentyp durch unterschiedliche Datentypen ersetzen.
Spezialisierung und Überladen von Funktionstemplates
Nun kommt der etwas schwierigere Teil: Was passiert, wenn die Funktion Max() mit zwei C-Strings (char-Zeiger) aufgerufen wird um Strings miteinander zu vergleichen?
#include <print>
#include <typeinfo>
// Templatedefinition
template <typename T>
T Max(const T p1, const T p2)
{
return ((p1>p2)? p1 : p2);
}
int main()
{
// 2 C-Strings definieren
const char *pstring1 = "XX", *pstring2 = "AAAA";
// C-Strings vergleichen
std::println("({} > {}) = {}",
pstring1,pstring2,Max(pstring1,pstring2));
}
(XX > AAAA) = AAAA
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 den 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.
#include <print>
#include <typeinfo>
#include <cstring>
// Allgemeine Templatedefinition
template <typename T>
T Max(const T p1, const T p2)
{
return ((p1>p2)? p1 : p2);
}
// Spezialisiertes Funktionstemplate
template<>
const char* Max<const char*>(const char *p1,
const char *p2)
// Alternative Schreibweise
// template <>
// const char* Max(const char *p1, const char *p2)
{
return ((std::strcmp(p1,p2) > 0)? p1 : p2);
}
int main()
{
// 2 C-Strings definieren
const char *pstring1 = "XX", *pstring2 = "AAAA";
// C-Strings vergleichen
std::println("({} > {}) = {}",
pstring1,pstring2,Max(pstring1,pstring2));
}
(XX > AAAA) = XX
Eine andere Möglichkeit, die C-Strings zu vergleichen, besteht darin, explizit 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.
Lokale Daten mit formalem Datentyp
Der formale Datentyp kann nicht nur bei Parametern eingesetzt werden, sondern auch bei der Definition von lokalen Daten. Bei der Generierung der Funktion durch den Compiler wird dieser formale Datentyp wieder durch den tatsächlichen Datentyp ersetzt.
#include <print>
#include <iostream>
import CData2;
// Allgemeine Templatedefinition
// Vertausch Inhalte von p1 und p2
template <typename T>
void Swap(T& p1, T& p2)
{
T temp{std::move(p1)}; // move-ctor
p1 = std::move(p2); // move operator =
p2 = std::move(temp); // move operator =
}
int main()
{
// 2 ints definieren
int ivar1 = 10, ivar2 = 20;
std::println("ivar1: {}, ivar2: {}",ivar1,ivar2);
// und Inhalte vertauschen
Swap(ivar1,ivar2);
std::println("ivar1: {}, ivar2: {}",ivar1,ivar2);
// 2 CData2 Objekte definieren
CData2 obj1{3}, obj2{5};
std::cout << "\nobj1: " << obj1;
std::cout << "obj2: " << obj2;
// und Objekte vertauschen
Swap(obj1,obj2);
std::cout << "\nobj1: " << obj1;
std::cout << "obj2: " << obj2;
}
ivar1: 10, ivar2: 20
ivar1: 20, ivar2: 10
obj1: Daten Objekt 1:
41, 67, 34,
obj2: Daten Objekt 2:
0, 69, 24, 78, 58,
obj1: Daten Objekt 1:
0, 69, 24, 78, 58,
obj2: Daten Objekt 2:
41, 67, 34,
Wie in Zeile 30 ersichtlich können Templates nicht nur intrinsiche Daten verarbeiten sondern auch Objekte. Die zu den Beispielen gehörige Klasse CData2 finden Sie im Anhang R.
Template-Parameter
Funktionstemplates können nicht nur einen sondern auch mehrere formale Datentypen besitzen.
template <typename T1, typname T2>
RTYP Func(T1 param1, T2 param2)
{
...
}
Im nachfolgenden Beispiel wird die Templatefunktion Func() mit zwei Argumenten aufgerufen bei denen die Datentypen unterschiedlich sein können.
#include <print>
#include <iostream>
#include <typeinfo>
#include <fstream>
import CData2;
// Dateiname fuer die Datenausgabe
constexpr const char *FILENAME="c:/temp/xxx.txt";
// Template mit 2 formalen Datentypen
// Gibt das Datum data inkl. dessen Typ auf
// den Ausgabestream out aus.
template <typename T1, typename T2>
void Out(T1& out, const T2& data)
{
// Ausgabe formatieren
// und ausgeben
out << "Datentyp: " << typeid(T2).name() << ", Wert: "
<< data << '\n';
}
int main()
{
// int und CData2 Objekt definieren
int ivar = 10;
CData2 obj{5};
// Daten auf Standardausgabe
Out(std::cout, ivar);
Out(std::cout, obj);
// Daten in Datei ablegen
std::ofstream outFile{FILENAME};
// Fehler abfangen!
if (!outFile)
{
std::println("Datei {} konnte nicht geoeffnet werden!",
FILENAME);
}
else
{
// Daten in Datei ablegen und Datei schliessen
Out(outFile, ivar);
Out(outFile, obj);
outFile.close();
std::println("Daten in {} abgelegt!",FILENAME);
}
}
Datentyp: i, Wert: 10
Datentyp: W6CData26CData2, Wert: Daten Objekt 1:
41, 67, 34, 0, 69,
Daten in c:/temp/xxx.txt abgelegt!
Neben Parametern mit formalen Datentypen können Funktionstemplates Parameter mit definierten Datentypen besitzen. Im Beispiel erhält das Funktionstemplate Add() als zweiten Parameter einen const char-Zeiger. Zusätzlich ist im Beispiel nochmals aufgeführt, wie schwerwiegende Fehler abgefangen werden können.
#include <print>
#include <cstring>
#include <stdexcept>
#include <charconv>
// Template mit einem formalen Datentypen
// und einem definierten Datentyp
// Konvertiert uebergebenen C-String in einen
// numerischen Wert und addiert ihn zu numVal
template <typename T1>
T1 Add(const T1& numVal, const char* stringVal)
{
// Stringwert konvertieren
T1 convVal;
auto res = std::from_chars(stringVal,
stringVal+std::strlen(stringVal),
convVal);
// Pruefen, ob Konvertierung erfolgreich
// andernfalls Ausnahme ausloesen
if (res.ec != std::errc())
throw std::invalid_argument("Konvertierungsfehler!");
// Datum entsprechend erhoehen
convVal += numVal;
return convVal;
}
int main()
{
// 2 Daten definieren
int ivar = 10;
float fvar = 3.14f;
std::println("ivar: {}, fvar: {}",ivar,fvar);
// Konvertierungsfehler verarbeiten
try
{
std::println("Add(ivar,\"10\"): {}",Add(ivar,"10"));
std::println("Add(fvar,\"1.1\"): {}",Add(fvar,"1.11"));
// Das loest eine Ausnahme aus
std::println("Add(ivar,\"notNumeric\"): {}",
Add(ivar,"notNumeric"));
}
// Konvertierungsfehler abfangen
catch(const std::invalid_argument& ex)
{
std::println("{}",ex.what());
}
}
ivar: 10, fvar: 3.14
Add(ivar,"10"): 20
Add(fvar,"1.1"): 4.25
Konvertierungsfehler!
Ebenfalls kann der Datentyp eines Template-Parameters explizit vorgegeben werden. Dazu wird der Datentyp beim Funktionsaufruf in spitzen Klammern nach dem Funktionsnamen angegeben. Im nachfolgenden Beispiel hat beim ersten Aufruf der Funktion Add() der formale Datentyp T1 den Datentyp int und beim zweiten Aufruf den Datentyp float.
#include <print>
#include <cstring>
#include <stdexcept>
#include <charconv>
// Template mit einem formalen Datentypen
// und einem definierten Datentyp
// Konvertiert uebergebenen C-String in einen
// numerischen Wert und addiert ihn zu numVal
template <typename T1>
T1 Add(const T1& numVal, const char* stringVal)
{
// Stringwert konvertieren
T1 convVal;
auto res = std::from_chars(stringVal,
stringVal+std::strlen(stringVal),
convVal);
// Pruefen, ob Konvertierung erfolgreich
// andernfalls Ausnahme ausloesen
if (res.ec != std::errc())
throw std::invalid_argument("Konvertierungsfehler!");
// Datum entsprechend erhoehen
convVal += numVal;
return convVal;
}
int main()
{
// int-Datum definieren
int ivar = 10;
try
{
// Add mit int-Berechnung ausfuehren
auto res1 = Add(ivar,"1.11");
std::println("Add(ivar,\"1.11\"): {}", res1);
// Add mit float-Berechnung ausfuehren
auto res2 = Add<float>(ivar,"1.11");
std::println("Add<float>(ivar,\"1.11\"): {}", res2);
}
catch(const std::invalid_argument& ex)
{
std::println("Fehler: {}",ex.what());
}
}
Add(ivar,"1.11"): 11
Add<float>(ivar,"1.11"): 11.11
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, d.h., ein non-type Parameter muss zur Compilezeit berechenbar sein.
Der Datentyp eines 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.
#include <print>
#include <type_traits>
#include <cstdlib>
// Funkionstemplate fuellt uebergebenes Feld mit
// Zufallszahlen im Bereich (LOW...HIGH]
// LOW, HIGH und ASIZE sind non-type Parameter
// Die Feldgroesse ASIZE wird automatisch durch den
// Compiler ermittelt da das Feld per Referenz
// uebergeben wird
template <typename T, int LOW = 0, int HIGH = 100, std::size_t ASIZE>
void FillArray(T(&field)[ASIZE])
{
// Uebergebenes Feld durchlaufen
for (auto& elem : field)
{
// Wenn T ein Integer-Datentyp ist
if (std::is_integral_v<T>)
// Zufallszahl im Bereich(HIGH-LOW) erzeugen
// und um Offset LOW verschieben
elem = (std::rand() % (HIGH - LOW)) + LOW;
else
// T ist ein (in unserem Fall) ein Gleitkomma-Datentyp
// Zufallszahl in Gleitkomma-Daten konvertieren und
// durch max. Zufallszahl teilen, ergibt Bereich
// (0...1]
// Ergebnis mit Zufallszahlenbereich multiplizieren
// und um Offset LOW verschieben
elem = (static_cast<T>(std::rand()) /
RAND_MAX) * (HIGH - LOW) + LOW;
}
}
int main()
{
// Feld mit 10 ints anlegen
int iarray[10];
// Ergibt folgende Funktion
// void FillArray(int (&iarray)[ASIZE]) und
// LOW=0, HIGH=100, ASIZE=10
FillArray(iarray);
std::println("FillArray<int, 0, 100>:");
for (auto elem : iarray)
std::print("{},", elem);
std::println();
// Ergibt folgende Funktion
// void FillArray(int (&iarray)[ASIZE]) und
// LOW=-5 HIGH=+5, ASIZE=10
std::println("FillArray<int, -5, +5>:");
FillArray<int, -5, +5>(iarray);
for (auto elem : iarray)
std::print("{},", elem);
std::println();
// Feld mit 3 floats anlegen
float farray[3];
// Ergibt folgende Funktion
// void FillArray(float (&iarray)[ASIZE]) und
// LOW=0, HIGH=100, ASIZE=10
FillArray<float,0,1>(farray);
std::println("FillArray<float, 0, 1>:");
for (auto index=0; index<3; index++)
std::print("{:.4f},", farray[index]);
std::println();
}
FillArray<int, 0, 100>:
41,67,34,0,69,24,78,58,62,64,
FillArray<int, -5, +5>:
0,0,-4,2,-4,-4,0,-3,2,1,
FillArray<float, 0, 1>:
0.9885,0.4457,0.1191,
Variadische Funktionstemplates
Variadische Funktionstemplates sind Templatefunktionen, welche eine variable Anzahl von Template-Parametern besitzen. Stellen Sie sich z.B. vor, Sie sollen ein Funktionstemplate entwickeln, das aus einer beliebigen Anzahl von Daten den Durchschnittswert berechnen soll. Dazu werden zwei 'Zwischendaten' benötigt: zum einen die Summe aller Daten und zum anderen die Anzahl der Daten.
Um in einem ersten Schritt zunächst die Summe von zwei beliebigen Daten zu berechnen, kann folgendes Template verwendet werden.
#include <print>
// Berechnet die Summe aus zwei Daten
// mit beliebigem Datentyp
template <typename T1, typename T2>
auto Sum(T1 val1, T2 val2)
{
return val1+val2;
}
int main()
{
auto res = Sum(2,3.14);
std::println("Summe von 2 und 3.14 ist: {:.2f}", res);
}
Summe von 2 und 3.14 ist: 5.14
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 den Funktionsparameter ebenfalls als parameter pack zu kennzeichnen, werden wiederum drei Punkte angegeben, diesmal vor dem Parameternamen. Und dieser Parameter enthält dann alle an die Funktion übergebenen Daten, die nicht vorher explizt weiteren Parametern zugewiesen wurden. D.h., in der Parameterliste eines Funktionstemplates muss ein parameter pack immer als letzter Parameter stehen.
// Variadisches Funktionstemplate
template <typename ...T>
auto Sum(???, T ...pack)
{
return ???;
}
Soll das parameter pack an eine weitere Templatefunktion übergeben werden, sind nach dem Parameternamen des parameter packs wieder drei Punkte zu setzen.
Sehen wir uns an, wie die vollständige Summenbildung aussieht:
#include <print>
// Spezialisiertes 'Ausstiegs'-Funktionstemplate fuer
// nur einen Parameter beim Funktionsaufruf
// Liefert lediglich den uebergebenen Wert zurueck
template <typename first>
auto Sum(first val)
{
return val;
}
// Funktionstemplate mit parameter pack
// Der erste uebergebene Wert wird dem Parameter 'val'
// zugewiesen und die restlichen uebergebenen Werte
// verbleiben im parameter pack 'rest'
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...);
}
int main()
{
// Summe der Werte ausgeben
std::println("Die Summe aus 1+2.4+100L ist {}",Sum(1, 2.4, 100L));
}
Die Summe aus 1+2.4+100L ist 103.4
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' 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 spezialisierten 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 <print>
// Spezialisiertes 'Ausstiegs'-Funktionstemplate fuer
// nur einen Parameter beim Funktionsaufruf
// Liefert lediglich den uebergebenen Wert zurueck
template <typename first>
auto Sum(first val)
{
return val;
}
// Funktionstemplate mit parameter pack
// Der erste uebergebene Wert wird dem Parameter 'val'
// zugewiesen und die restlichen uebergebenen Werte
// verbleiben im parameter pack 'rest'
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
// Erhaelt die Werte als parameter pack uebergeben
template <typename ...ppack>
auto Average(ppack ...data)
{
// Uebergebe parameter pack komplett an Summen-Funktion
// und dividiere die Summe durch Anzahl der Werte
// im parameter pack
return Sum(data...)/sizeof...(data);
}
int main()
{
// Summe der Werte ausgeben
std::println("Der Mittelwert aus 1+2.4+100L ist {:.4f}",
Average(1, 2.4, 100L));
}
Der Mittelwert aus 1+2.4+100L ist 34.4667
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.
// Datei templ.h
template <typename T>
void Swap (T v1, T2 v2)
{
... // Anweisungen
}
// Datei source1.cpp
#include "templ.h"
void Func1()
{
int var1,var2;
...
Swap(var1, var2); // Instanz. des Funktionstpl.
...
}
// Datei source2.cpp
#include "templ.h"
extern template <typename T> Swap(T,T);
void Func2()
{
int x1,x2;
...
Swap(x1, x2); // Keine Instanziierung der Tpl-Funktion
...
}
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.
Funktionstemplates in Modulen
Bei der Definition von Funktionstemplates in Modulen ist besondere Vorsicht geboten. Wie bereits erwähnt, instanziiert der Compiler die Funktion erst wenn er einen entsprechenden Funktionsaufruf sieht. D.h., das Modul selbst enthält nicht den Funktionscode, sondern nur die im Modul definierten Namen. Und das hat entsprechende Auswirkungen. Sehen Sie sich dazu einmal das folgende Beispiel an:
// Datei mit der Templatedefinition
module;
// Bibliotheksdateien einbinden
#include <iostream>
#include <cstdlib>
export module Test;
export template<typename T>
void Print(T val)
{
std::cout << "Wert = " << std::rand() % val << '\n';
std::cout << "Wert = " << std::rand() % val << '\n';
}
// Instanziierung der Templatefunktion
import Test;
int main()
{
Print(10);
}
Wert = 110Wert = 710
Die Ausgabe der in der Funktion 'berechneten' Werte erfolgt nicht wie in der Funktion angegeben. Der Grund hierfür ist, dass zwar cout und rand() beim Übersetzen des Moduls bekannt sind aber nicht exportiert werden.
Die 'sauberste' Lösung für diese Problem ist, im Modul iostream und cstdlib zu importieren anstelle mittels #include einzubinden. Dies funktioniert bis jetzt (Stand: Februar 2026) aber leider nur mit der MS Visual Studio IDE.
// Datei mit der Templatedefinition
export module Test;
// Standardbibliothek importieren
import <iostream>;
import <cstdlib>;
export template<typename T>
void Print(T val)
{
std::cout << "Wert = " << rand() % val << '\n';
std::cout << "Wert = " << rand() % val << '\n';
}
Eine zweite, und zum jetzigen Zeitpunkt einzige portable, Lösung ist, iostream und cstdlib mit in der Datei einzubinden, die die Templatefunktion instanziiert.
// Instanziierung der Templatefunktion
// Bibliotheksfunktion zusaetzlich einbinden
#include <cstdlib>
#include <iostream>
import Test;
int main()
{
Print(10);
}
Wert = 1
Wert = 7
Werden Funktionen aus der Standardbibliothek verwendet und der eingesetzte Compiler bietet keine Möglichkeit diese aus der Standardbibliothek zu importieren, sollten alle Templates in einer Header-Datei definiert werden und nicht in einer Moduldatei. Nur so ist gewährleistet, dass bei der Instanziierung der richtige Code erzeugt wird.
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 noch 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 <print>
#include <numbers>
// Variablentemplate
// pi_v ist eine C++ Standardkonstante, definiert im
// Header numbers
template<typename T> constexpr T pi = std::numbers::pi_v<T>;
template <typename T>
T Kreisumfang(T radius)
{
return static_cast<T>(2.0) * radius * pi<T>;
}
int main()
{
std::println("Berechnung mit double: {}", Kreisumfang(3.0));
std::println("Berechnung mit float : {}", Kreisumfang(3.0f));
}
Berechnung mit double: 18.84955592153876
Berechnung mit float : 18.849556
Je nach dem, ob an die Templatefunktion Kreisumfang() ein float- oder double-Wert übergeben wird, wird für die Berechnung auch die in der Standardbibliothek definierte Konstante pi_v mit der entsprechenden Genauigkeit verwendet.
fold expression
Durch den Einsatz einer fold expression können binäre Operatoren auf eine variable Anzahl von Template-Parameter angewandt werden. Dies kann zwar auch mit einem variadischen Funktionstemplate erreicht werden, jedoch ist dabei i.d.R. ein 'Ausstiegs'-Template erforderlich.
Eine fold expression wir wie folgt definiert:
(... OP ppack) bzw.
(ppack OP ...)
OP ist der binäre Operator, der auf die Daten im parameter pack ppack angewandt wird. Er kann sein ein
- arithmetischer Operator, z.B. + oder %
- ein Bit-Operator, z.B. & oder ^
- Vergleichsoperator, z.B. < oder ==
- Schiebeoperator << oder >>
- Logikoperator && oder ||
- Memberzeiger .* oder ->*
- Komma-Operator ,
sowie deren Kurzschreibweisen.
Mithilfe der fold expression kann das Beispiel aus dem Abschnitt Variadische Funktionstemplates für die Berechnung des Mittelwertes wie folgt vereinfacht werden:
#include <print>
// Funktionstemplate zur Mittelwertberechnung
// Erhaelt die Werte als parameter pack uebergeben
template <typename ...ppack>
auto Average(ppack ...data)
{
// Addiert alle Werte des parameter packs
// mithilfe einer fold expression
auto sum = (data + ...);
// Mittelwert ist Summe der Werte im parameter pack
// geteilt durch Anzahl Werte im parameter pack
return sum / sizeof...(data);
}
int main()
{
// Summe der Werte ausgeben
std::println("Der Mittelwert aus 1+2.4+100L ist {:.4f}",
Average(1, 2.4, 100L));
}
Der Mittelwert aus 1+2.4+100L ist 34.4667
Und noch ein weiteres Beispiel für die Anwendung einer fold expression:
#include <print>
// Berechnet die Summe aller im
// parameter pack enthaltenen Daten
template <typename ...ppack>
auto Sum(ppack ...data)
{
auto sum = (data + ...);
return sum;
}
// Gibt mittels println() alle im
// parameter pack enthaltenen Daten aus
template <typename ...ppack>
void PrintAll(ppack ...data)
{
int i=0; // Datenindex
(std::println("{}: {}",++i,data), ...);
}
int main()
{
// Daten fuers parameter pack
int ivar=10;
float fvar = 3.12f;
bool bvar = true;
// Summe der Daten berechnen (true=1)
std::println("Summe: {}",Sum(ivar,fvar,bvar));
// Alle Daten ausgeben
PrintAll(ivar,fvar,bvar);
}
Summe: 14.12
1: 10
2: 3.12
3: true
Wie zu sehen ist, können nicht nur Konstanten sondern auch Variablen im parameter pack übergeben werden.
Besonders deutlich wird die Arbeitsweise der fold expression in Zeile 18. Der Parameter data enthält das aktuell verarbeitet Datum des packs und ruft damit die Bibliotheksfunktion println() auf. Der Operator ist hier der Komma-Operator!
Fehlt noch der Unterschied zwischen
(... 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 <print>
// Funktionstemplate mit right fold expression
template <typename ...ppack>
auto SubRight(ppack ...data)
{
// Subtrahiert alle Werte des parameter packs
return (data - ...);
}
// Funktionstemplate mit left fold expression
template <typename ...ppack>
auto SubLeft(ppack ...data)
{
// Subtrahiert alle Werte des parameter packs
return (... - data);
}
// main Funktion
int main()
{
// Wert subtrahieren
std::println("(data - ...): {}",SubRight(1,2,3,4));
std::println("(... - data): {}",SubLeft(1,2,3,4));
}
(data - ...): -2
(... - data): -8
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.
Übungen
ftempl_01:
Es ist ein Funktionstemplate Sort() zum Sortieren von numerischen Daten zu erstellen. Das eventl. Tauschen der Daten ist über ein weiteres Funktionstemplate Swap() umzusetzen.
Die Templates sind in einer Modul-Datei zu definieren.
Legen Sie ein long- und ein double-Feld an und initialisieren es mit beliebig vielen Werten. Die Feldgröße ist nicht explizit vorzugeben, sondern sie soll sich nach der Anzahl der Initialwerte bei der Definition des Feldes richten.
Beide Felder sind im unsortierten und sortierten Zustand auszugeben.
Unsortierte long:
1, -10, -2, 20,
Sortierte long:
-10, -2, 1, 20,
Unsortiert double:
1.1, 0.9, -1.2, 5.5,
Sortierte double:
-1.2, 0.9, 1.1, 5.5,
ftempl_02:
Mithilfe der in der vorherigen Übung ftempl_01 erstellten Funktionstemplates Swap() und Sort() soll eine Adressenliste alphabetisch sortiert werden, wobei eine Adresse nur aus den beiden string-Eigenschaften Name und Wohnort besteht. Für die Ausgabe der Adresse ist u.a. der Operator << zu überladen.
In main() ist eine Adressenliste für vier Einträge dynamisch zu erstellen und mit folgenden Daten zu initialisieren:
Name: Karl Maier Ort: AStadt
Name: Agathe Mueller Ort: XDorf
Name: Xaver Lehmann Ort: CHausen
Name: Berta Schmitt Ort: FStadt
Sortieren Sie die Adressenliste mithilfe der beiden Funktionstemplates Sort() und Swap() alphabetisch nach Namen.
In der Modul-Datei mit den Funktionen Sort() und Swap() sind keine Anpassungen notwendig.
Adressen unsortiert:
Name: Karl Maier, Ort: AStadt
Name: Agathe Mueller, Ort: XDorf
Name: Xaver Lehmann, Ort: CHausen
Name: Berta Schmitt, Ort: FStadt
Adressen sortiert:
Name: Agathe Mueller, Ort: XDorf
Name: Berta Schmitt, Ort: FStadt
Name: Karl Maier, Ort: AStadt
Name: Xaver Lehmann, Ort: CHausen
ftempl_03:
Es ist ein Funktionstemplate zur byteweisen Ausgabe eines beliebigen Integer-Datentyps zu implementieren. Für die Ausgabe sind die einzelnen Bytes des Datums in ASCII-Zeichen umzuwandeln, d.h. ein unsigned char-Wert von 10, entspricht Hex 0x0a, ist als '0' 'a' auszugeben.
Damit der Empfänger der Daten diese korrekt auswerten kann, ist zusätzlich der Datentyp des übertragenen Datums mit auszugeben.
Die Ausgabefunktion soll neben dem auszugebenden Wert den für die Übertragung zu verwendenden Ausgabestream (z.B. cout oder ofstream) als Parameter erhalten.
Geben Sie folgende Daten aus:
eine int-Variable mit dem Wert 10,
eine char-Variable mit dem Zeichen 'A'.
das Literal 0x61,
das Literal 0x61L,
das char-Literal 0x61 und
das short-Literal 0x1234
Verwenden Sie keine Typkonvertierung für die Literale.
sending 10 as bytes: i 0a 00 00 00
sending A as bytes: c 41
sending 97 as bytes: i 61 00 00 00
sending 97L as bytes: l 61 00 00 00
sending char 97 as bytes: c 61
sending short 4660 as bytes: s 34 12
ftempl_04:
Erweitern Sie die vorherige Übung so, dass auch C-Strings byteweise ausgegeben werden können.
sending 10 as bytes: i 0a 00 00 00
sending A as bytes: c 41
sending 97 as bytes: i 61 00 00 00
sending 97L as bytes: l 61 00 00 00
sending char 97 as bytes: c 61
sending short 4660 as bytes: s 34 12
sending C-String Ein Text as bytes: PKc E i n T e x t