Template-Spezialitäten
Template-Objekte als Parameter
Sollen an eine Funktion/Methode Objekte eines Klassentemplates übergeben werden, so ist diese ebenfalls als Template zu definieren. Dabei sind zwei Fälle zu unterscheiden:
1. Fall: Funktion erhält stets Objekte des gleichen Templates
In diesem Fall ist als Datentyp des Parameters der Name des Klassentemplates anzugeben, gefolgt vom formalen Datentyp in spitzen Klammern. Der formale Datentyp des Funktionstemplates entspricht dem Datentyp, mit dem das übergebene Klassentemplate-Objekt instanziiert wurde. Innerhalb des Funktionstemplates können somit Daten vom Typ des formalen Template-Arguments definiert werden.
#include <print>
// Klassentemplate
template <typename T>
class SaveData
{
T data; // Beliebiges Datum
public:
// Standard-ctor
SaveData()
{
data = T{};
}
// ctor mit abzulegendem Datum
SaveData(const T& val)
{
data = val;
}
// Datum zurueckgeben
T GetData() const
{
return data;
}
};
// Funktionstemplate, erhaelt immer ein Objekt
// vom Typ des Klassentemplates SaveData
template <typename T>
void PrintTypeVal(const SaveData<T>& obj)
{
// Datentyp und Wert des im Klassentemplate
// abgelegten Datums ausgeben
std::println("Template-Datentyp: {}, Datum: {}",
typeid(T).name(), obj.GetData());
}
int main()
{
// int und float Datum in der Klasse ablegen
SaveData intObj(1);
SaveData floatObj(3.14f);
// Datentyp und Datum ausgeben
PrintTypeVal(intObj);
PrintTypeVal(floatObj);
}
Template-Datentyp: int, Datum: 1
Template-Datentyp: float, Datum: 3.14
2. Fall: Funktion erhält Objekte unterschiedlicher Templates
Jetzt ist als Datentyp des Funktionsparameters der formale Datentyp des Funktionstemplates anzugeben, der nun vom Typ des übergebenen Klassentemplates ist. Bleibt ein kleines Problem bestehen, das es zu lösen gilt, wenn aus der Templatefunktion heraus eine Methode/Funktion aufgerufen wird, die einen vom formalen Datentyp des übergebenen Klassentemplates abhängigen Returnwert zurückliefert. Da der Compiler beim Übersetzen des Programms den Returnwert der aufgerufenen Methode/Funktion kennt, lassen wir ihn den Datentyp der Variable bzw. des Objekts bestimmen, indem wir den Datentyp auto verwenden.
#include <print>
#include <typeinfo>
// 1. Klassentemplate
template <typename T>
class SaveData
{
T data; // Beliebiges Datum
public:
// ctor mit abzulegendem Datum
SaveData(const T& val) : data(val)
{ }
// Datum zurueckgeben
T GetData() const
{
return data;
}
};
// 2. Klassentemplate
template <typename T>
class SaveData2
{
T data; // Beliebiges Datum
public:
// ctor mit abzulegendem Datum
SaveData2(const T& val) : data(val)
{ }
// Datum zurueckgeben
T GetData() const
{
return data * 2;
}
};
// Funktionstemplate, erhaelt Objekte von
// verschiendenen Klassentemplates
template <typename T>
void PrintTypeVal(const T& obj)
{
// Datentyp von T ausgeben
std::println("Datentyp von T: {}", typeid(T).name());
// Datentyp und Wert des im Klassentemplate
// abgelegten Datums ausgeben
std::println("Template-Datentyp: {}, Datum: {}",
typeid(decltype(obj.GetData())).name(), obj.GetData());
}
int main()
{
// int und float Datum in der Klasse ablegen
SaveData intObj(1);
SaveData2 floatObj(3.14f);
// Datentyp und Datum ausgeben
PrintTypeVal(intObj);
PrintTypeVal(floatObj);
}
Datentyp von T: class SaveData<int>
Template-Datentyp: int, Datum: 1
Datentyp von T: class SaveData2<float>
Template-Datentyp: float, Datum: 6.28
Übung
tparam_01:
Vorgegeben sind folgende zwei Funktionstemplates für den Vergleich von beliebigen Daten:
// Funktionstemplate
// Liefert true zurueck wenn val1<val2
template <typename T>
bool Lesser(T& val1, T& val2)
{
return val1<val2;
}
// Funktionstemplate
// Liefert true zurueck wenn val1>val2
template <typename T>
bool Greater(T& val1, T& val2)
{
return val1>val2;
}
Implementieren Sie eine Funktion Sort() zum Sortieren von int-Daten. Die Funktion erhält als Parameter das zu sortierende int-Feld, die Größe des Feldes und ein Sortierkriterium. Für das Sortierkriterium ist eine der beiden oben angegebenen Funktionstemplates einzusetzen. Ein möglicher Aufruf von Sort() könnte damit wie folgt aussehen:
Sort(iarray, size, Lesser<int>);
Legen Sie ein Feld für 10 int-Werte an, füllen es mit Zufallszahlen und geben es aus. Sortieren Sie die Feldelemente sowohl in aufsteigender als auch in fallender Reihenfolge. Geben Sie das sortierte Feld jeweils aus.
Beachten Sie, dass die Funktion Sort() einen Zeiger auf das Funktionstemplate zum Vergleichen der Elemente erhält!
Ausgangsfeld:
41,67,34,0,69,24,78,58,62,64,
Aufsteigend sortiert:
0,24,34,41,58,62,64,67,69,78,
Fallend sortiert:
78,69,67,64,62,58,41,34,24,0,
Klassentemplates und Ableitungen
Beim Ableiten von Klassentemplates können drei Fälle auftreten:
1. Fall: Basisklasse ist ein definiertes Klassentemplate
In diesem Fall erfolgt die Ableitung wie gewohnt, d.h., nach dem Zugriffsrecht der Ableitung folgt der Name der Basisklasse. Da die Basisklasse ein Klassentemplate ist, ist zusätzlich in spitzen Klammern der Datentyp anzugeben, der für den formalen Datentyp in der Basisklasse einzusetzen ist.
Ist der formale Datentyp des Basisklassen-Templates ein definierter Datentyp, kann die abgeleitete Klasse als normale Klasse definiert werden (erste Ableitung SubNorm). Variiert der Datentyp, ist die abzuleitende Klasse ebenfalls als Klassentemplate zu definieren, wobei das Template-Argument an die Basisklasse weitergegeben wird (zweite Ableitung SubTemp)
#include <print>
#include <string>
// Basisklasse
template <typename T>
class Base
{
T data; // beliebiges Datum
public:
// ctor mit abzulegendem Datum
Base(const T& val): data(val)
{}
// Datum zurueckgeben
virtual T GetData() const
{
return data;
}
};
// Abgeleitete Klasse, Basisklassen-Template
// hat einen definierten Datentyp (hier int)
class SubNorm: public Base<int>
{
std::string text; // bel. Datum
public:
// ctor
SubNorm(const int val):
Base(val), text("Basisdatentyp: fix")
{}
// Daten ausgeben
void Print() const
{
auto res = GetData();
std::println("{}, Daten: {}, Typ: {}",
text, res, typeid(res).name());
}
};
// Abgeleitete Klasse, Basisklassen-Template
// kann verschiende Datentypen besitzen
template <typename T>
class SubTemp: public Base<T>
{
std::string text; // bel. Datum
public:
// ctor mit abzulegendem Datum
SubTemp(const T& val):
Base<T>(val), text("Basisdatentyp: variable")
{}
// Daten ausgeben
void Print() const
{
auto res = Base<T>::GetData();
// Alternative:
// auto res = this->GetData();
std::println("{}, Daten: {}, Typ: {}",
text, res, typeid(res).name());
}
};
int main()
{
// 3 Objekte anlegen
SubNorm normObj(10); // T der Basisklasse = int
SubTemp<float> tplObj1(3.14f); // T der Basisklasse = float
SubTemp<char> tplObj2('a'); // T der Basisklasse = char
// Daten ausgeben
normObj.Print();
tplObj1.Print();
tplObj2.Print();
}
Basisdatentyp: fix, Daten: 10, Typ: int
Basisdatentyp: variable, Daten: 3.14, Typ: float
Basisdatentyp: variable, Daten: a, Typ: char
Ist die abgeleitete Klasse ebenfalls ein Klassentemplate und es soll auf die public- oder protected-Member der Basisklasse zugegriffen werden, muss der Zugriff über den this-Zeiger oder qualifiziert erfolgen (siehe Zeile 53)!
2. Fall: Basisklasse sind unterschiedliche Klassen
Hierbei ist die abzuleitende Klasse immer als Klassentemplate zu definieren. Bei der Ableitung ist die Basisklasse als formaler Datentyp anzugeben.
#include <print>
#include <string>
#include <string_view>
using namespace std::string_literals;
// 1. Basisklasse
class Car
{
std::string name;
public:
Car(std::string_view _name) : name(_name)
{ }
const std::string GetData() const
{
return "PKW: "s + name;
}
};
// 2. Basisklasse
class Truck
{
std::string name;
public:
Truck(std::string_view _name) : name(_name)
{
}
const std::string GetData() const
{
return "LKW: "s + name;
}
};
// Abgeleitete Klasse
// Erhaelt Basisklasse als Templateparameter TBase
template <typename TBase>
class Vehicle : public TBase
{
float weight;
public:
// ctor, ruft per TBase() den ctor
// der aktuellen Basisklasse auf
Vehicle(std::string_view name, float _weight):
TBase(name), weight(_weight)
{ }
// Daten ausgeben
void Print() const
{
std::println("{}, Gewicht: {}t", TBase::GetData(), weight);
}
};
int main()
{
// Basisklasse von Vehicle ist Car
Vehicle<Car> carObj("Golf", 1.6f);
// Basisklasase von Vehicle ist nun Truck
Vehicle<Truck> truckObj("Actros", 42.0f);
// Daten ausgeben
carObj.Print();
truckObj.Print();
}
PKW: Golf, Gewicht: 1.6t
LKW: Actros, Gewicht: 42t
3. Fall: Basisklassen sind unterschiedliche Klassentemplates
Auch hier ist die abzuleitende Klasse als Klassentemplate zu definieren, die im formalen Datentyp das Klassentemplate der Basisklasse erhält.
Bei der Definition eines Objekts der abgeleiteten Klasse ist das Basisklassen-Template inklusive dessen Template-Argument anzugeben.
#include <print>
#include <string>
#include <string_view>
using namespace std::string_literals;
// 1. Basisklassen-Template
template <typename T>
class BasisTpl1
{
T data;
std::string text;
public:
BasisTpl1(T _data) : data(_data), text("BasisTpl1"s)
{
}
const std::string GetData() const
{
return std::format("{} mit {} Datum: {}",
text, typeid(T).name(), data);
}
};
// 2. Basisklassen-Template
template <typename T>
class BasisTpl2
{
T data;
std::string text;
public:
BasisTpl2(T _data) : data(_data), text("BasisTpl2"s)
{
}
const std::string GetData() const
{
return std::format("{} mit {} Datum: {}",
text, typeid(T).name(), data);
}
};
// Abgeleitete Klasse
// Erhaelt Basisklassen-Template als Templateparameter TBase
template <typename TBase, typename TData>
class Sub : public TBase
{
char seperator;
public:
// ctor, ruft per TBase() den ctor des
// aktuellen Basisklassen-Templates auf
Sub(char sep, TData data) :
TBase(data), seperator(sep)
{
}
// Daten ausgeben
void Print() const
{
std::println("Sub {} {}", seperator, TBase::GetData());
}
};
int main()
{
// Klasse Sub mit Basisklasse BasisTpl1<char>
Sub<BasisTpl1<char>, char> obj1Tpl1('-', 'A');
// Klasse Sub mit Basisklasse BasisTpl1<float>
Sub<BasisTpl1<float>, float> obj2Tpl1('|', 3.14f);
// Klasse Sub mit Basisklasse BasisTpl2<int>
Sub<BasisTpl2<int>, int> obj1Tpl2(',', 10);
// Daten ausgeben
obj1Tpl1.Print();
obj2Tpl1.Print();
obj1Tpl2.Print();
}
Sub - BasisTpl1 mit char Datum: A
Sub | BasisTpl1 mit float Datum: 3.14
Sub , BasisTpl2 mit int Datum: 10
Übung
tablei_01:
Vorgegeben ist das nachfolgende Klassentemplate Rotary für einen Drehschalter.
#include <limits>
#include <print>
#include <string>
// Basisklassen-Template für 'normalen' Drehschalter
// Default-Datentyp des Drehschalters ist unsigned short
template <typename T = unsigned short>
class Rotary
{
protected:
T value; // akt. Wert
T minValue; // kleinster Wert
T maxValue; // groesster Wert
public:
// ctor, initialisiert Eigenschaften
Rotary()
{
// Min/Max-Werte auf Bereichsgrenzen des
// aktuellen Datentyps setzen
value = minValue = std::numeric_limits<T>::min();
maxValue = std::numeric_limits<T>::max();
}
// Drehschalter um eins erhoehen
// aber nicht ueber obere Grenze
void Increment()
{
if (value < maxValue)
value++;
}
// Drehschalter um eins verringern
// aber nicht unter untere Grenze
void Decrement()
{
if (value > minValue)
value--;
}
// Eigenschaften ausgeben
void ShowProperties()
{
std::println("Min/Max: ({}/{}), akt. Wert: {}",
minValue, maxValue, value);
}
};
Die Klasse Rotary realisiert einen Drehschalter, der über Methoden inkrementiert und dekrementiert werden kann. Da der Drehschalter keinen festen Wertebereich haben soll, wird der Datentyp des Drehschalters über das Template-Argument definiert.
Ihre Aufgabe ist es, vom Klassentemplate Rotary eine Klasse TextRotary abzuleiten, die anstelle eines numerischen Wertes einen Text ausgibt.
Dem Konstruktor der Klasse TextRotary sind in einem Feld so viele Strings zu übergeben, wie der Button Zustände besitzen soll. Der Minimalwert eines TextRotary ist immer 0.
Legen Sie ein Objekt der Klasse Rotary mit dem Datentyp unsigned char an und geben dessen Eigenschaften aus. Inkrementieren Sie den Drehschalter und geben die Eigenschaften erneut aus.
Anschließend ist ein Objekt vom Typ TextRotary zu definieren, welches die Texte "OFF", "LOW" und "HIGH" ausgeben soll.
// Text fuer Text-Drehschalter
std::string textList[] {"OFF"s,"LOW"s,"HIGH"s};
// Text-Drehschalter definieren
TextRotary textRot(textList);
Inkrementieren Sie den Drehschalter einmal und anschließend dekrementieren Sie ihn zweimal. Geben Sie nach jeder Aktion dessen Eigenschaften aus.
Rotary -> Min/Max: (-128/127), akt. Wert: -128
Inkr. Rotary -> Min/Max: (-128/127), akt. Wert: -127
Text-Rotary -> Min/Max: (OFF/HIGH), akt. Wert: OFF
Inkr. Text-Rotary -> Min/Max: (OFF/HIGH), akt. Wert: LOW
Dekr. Text-Rotary -> Min/Max: (OFF/HIGH), akt. Wert: OFF
Dekr. Text-Rotary -> Min/Max: (OFF/HIGH), akt. Wert: OFF
Membertemplates
Membertemplates in Klassen
Auch einzelne Methoden in 'gewöhnlichen' Klassen können als Templates definiert werden. Diese Templates werden als Membertemplates bezeichnet. Mithilfe von Membertemplates kann unter Umständen unnötiger Code vermieden werden, da nicht mehrere überladene Methoden im Voraus definiert werden müssen.
Die Definition eines Membertemplates innerhalb der Klasse erfolgt auf die gleiche Weise wie ein Funktionstemplate.
Die Klasse, die das Membertemplate enthält, sowie entsprechende Objekte werden ohne Templateanweisung definiert.
Im nachfolgenden Beispiel sind zwei unabhängige Klassen Image und Text definiert. Für die Ausgabe der Eigenschaften enthalten beide Klassen eine Methode Print().
Die seitenweise Ausgabe von Objekten steuert die Klasse PrintPage über die Methode PrintPg(), die das auszugebende Objekt als Parameter erhält. Da die auszugebenden Objekte in keinerlei Beziehung zu einer stehen, muss PrintPg() als Membertemplate definiert werden. Die einzige Bedingung dabei ist, dass alle auszugebenden Objekte die Methode Print() definieren.
#include <print>
#include <string>
// Klasse zur Ausgabe eines Bildes
class Image
{
std::string imgName; // Bildname
public:
Image(std::string_view _imgName): imgName(_imgName)
{ }
// Bild ausgeben
void Print() const
{
std::println("Bild '{}'", imgName);
}
};
// Klasse zur Ausgabe eines Textes
class Text
{
std::string text; // auszugebender Text
public:
Text(std::string_view _text): text(_text)
{ }
// Text ausgeben
void Print() const
{
std::println("Text '{}'", text);
}
};
// Klasse zur Ausgabe einer Seite
class PrintPage
{
int pageNum; // Seitennummer
public:
// ctor, gibt Seitennummer aus
PrintPage(): pageNum(1)
{
std::println("--- Seite {} ---", pageNum);
}
// Ausgabe des uebergebenen Objekts
// Das Objekt muss die Methode Print() definieren
template <typename T>
void PrintPg(const T& obj) const
{
obj.Print();
}
// Neue Seite ausgeben
void NewPage()
{
std::println("--- Seite: {} ---", ++pageNum);
}
};
int main()
{
// 2 Bildobjekte und ein Textobjekt
Image imgObj1{"Bild1"}, imgObj2{"Bild2"};
Text txtObj{ "Ich bin ein Textobjekt" };
// Objekt fuer Ausgabe
PrintPage thePage;
// Ein Bild- und Textobjekt ausgeben
thePage.PrintPg(imgObj1);
thePage.PrintPg(txtObj);
// Neue Seite
thePage.NewPage();
// Bild auf neuer Seite ausgeben
thePage.PrintPg(imgObj2);
}
--- Seite 1 ---
Bild 'Bild1'
Text 'Ich bin ein Textobjekt'
--- Seite: 2 ---
Bild 'Bild2'
Wird das Membertemplate nicht innerhalb der Klasse definiert, sondern außerhalb, ist der Methode eine template-Anweisung voranzustellen. Hierbei ist zu beachten, dass nach dem Klassennamen kein Template-Parameter angegeben wird.
class PrintPage
{
...
// Deklaration Membertemplate
template <typename T>
void PrintPg(T obj);
...
};
// Definition
template <typename T>
void PrintPage::PrintPg(const T& obj) const
{
obj.Print();
}
Membertemplates in Klassentemplates
Wird das Membertemplate innerhalb eines Klassentemplates definiert, ergibt sich keine Abweichung zum vorherigen Vorgehen. Das Einzige auf das zu achten ist, ist, dass für die Template-Argumente des Klassentemplates und denen des Membertemplates unterschiedliche Bezeichner für die formalen Datentypen verwendet werden. Wenn für das Klassentemplate und das Membertemplate die gleichen formalen Datentypen verwendet werden, verdeckt der Bezeichner des Membertemplates den Bezeichner des Klassentemplates.
... Klasse Image und Text wie zuvor
// Klasse zur Ausgabe einer Seite
template <typename TPage>
class PrintPage
{
int pageNum; // Seitennummer
TPage prefix; // Seitenbeschriftung
public:
// ctor, gibt Seitennummer aus
PrintPage(TPage pref) : pageNum(1), prefix(pref)
{
std::println("--- {} {} ---", prefix,pageNum);
}
// Ausgabe des uebergebenen Objekts
// Das Objekt muss die Methode Print() definieren
template <typename Tobj>
void PrintPg(const Tobj& obj) const
{
obj.Print();
}
};
int main()
{
// 2 Bildobjekte und ein Textobjekt
Image imgObj1{ "Bild1" }, imgObj2{ "Bild2" };
Text txtObj{ "Ich bin ein Textobjekt" };
// Objekt fuer Ausgabe mit char-Prefix
PrintPage thePageNum('#');
// Ein Bild- und Textobjekt ausgeben
thePageNum.PrintPg(imgObj1);
thePageNum.PrintPg(txtObj);
// Neue Ausgabe mit const char-Prefix
PrintPage thePageString("Seite: ");
thePageString.PrintPg(imgObj2);
}
--- # 1 ---
Bild 'Bild1'
Text 'Ich bin ein Textobjekt'
--- Seite: 1 ---
Bild 'Bild2'
Etwas komplizierter wird die Sache, wenn das Membertemplate außerhalb des Klassentemplates definiert wird. In diesem Fall sind zwei template-Anweisungen anzugeben, eine für das Klassentemplate und eine für das Membertemplate.
// Klasse zur Ausgabe einer Seite
template <typename TPage>
class PrintPage
{
... // Deklaration Membertemplate
template <typename Tobj>
void PrintPg(const Tobj& obj) const;
};
// Definition
template <typename TPage>
template <typename Tobj>
void PrintPage<TPage>::PrintPg(const Tobj& obj) const
{
obj.Print();
}
Übung:
tmemtpl_01:
Implementieren Sie eine Klasse SaveFile zum Schreiben und Lesen von beliebigen Daten in eine Textdatei. Der Konstruktor der Klasse erhält als Parameter den Dateinamen und öffnet die Datei.
Außer dem Konstruktor und dem Destruktor, der die Datei wieder schließt, sind die Methoden
- Reset(), zum Zurücksetzen der Dateizeiger,
- Write(), zum Ablegen eines Datums in der Datei und
- Read(), zum Auslesen eines Datums
zu implementieren.
Beim Schreiben eines Datums ist zusätzlich zum Datum dessen Datentyp mit abzulegen.
Beim Einlesen eines Datums ist prüfen, ob der Datentyp des einzulesenden Datums mit dem Datentyp des in der Datei abgelegten Datums übereinstimmt. Stimmen die Datentypen nicht überein, ist eine Ausnahme auszulösen.
Verwenden Sie zur Prüfung der Datentypen eine if constexpr Anweisung. Beachten Sie, dass je nach Datentyp das aus der Datei eingelesene Datum noch in einen numerischen Wert konvertiert werden muss.
Damit nicht für jeden möglichen Datentyp eine Write() und Read() Methode zu schreiben ist, sind diese Methoden als Membertemplates zu realisieren.
Legen Sie nacheinander einen char- und long double-Wert sowie ein string-Objekt in einer Datei ab. Lesen Sie die Werte dann wieder ein und geben diese zur Kontrolle wieder aus.
Versuchen Sie einmal, einen 'falschen' Datentyp einzulesen.
Geschriebene Daten: A, 1.1, Dies ist ein String
Eingelesene Daten: A, 1.1, Dies ist ein String
Variadische Membertemplates
Analog zu variadischen Funktionstemplates können auch Membertemplates variadisch sein, d.h., sie können eine beliebige Anzahl von Parameter erhalten. Sehen wir uns das folgende Beispiel an.
#include <print>
#include <string>
using namespace std::string_literals;
// Klassentemplate zur Ablage von beliebige Datenelementen
template <typename pType>
class VArray
{
pType *pData = nullptr; // Zeiger auf Datenfeld
size_t noOfElements; // Anzahl der Daten
int index = 0; // Datenfeld Index
public:
// Variadischer ctor
// Erhaelt die Daten als parameter pack
template <typename ...args>
VArray(args ... plist)
{
// Anzahl der Elemente berechnen
noOfElements = sizeof...(plist);
// Datenfeld anlegen
pData = new pType[noOfElements];
// Daten ins Feld übernehmen (fold Expression!)
((pData[index++] = plist), ...);
}
// dtor, gibt Datenfeld wieder frei
~VArray()
{
delete[] pData;
}
// Ausgabe der Daten
void Print()
{
for (size_t i = 0; i < noOfElements; i++)
std::print("{}, ", pData[i]);
std::println("");
}
};
int main()
{
// Feld mit int-Daten anlegen und ausgeben
VArray<int> intArray{ 1,2,3,4 };
std::println("intArray:");
intArray.Print();
// Feld mit strings anlegen und ausgeben
VArray<std::string> sArray{ "eins"s, "zwei"s, "drei"s };
std::println("sArray:");
sArray.Print();
}
intArray:
1, 2, 3, 4,
sArray:
eins, zwei, drei,
Das Klassentemplate VArray dient zur Ablage von beliebigen vielen Daten eines Datentyps in einem Feld. Die abzulegenden Daten werden in einer Liste an den Konstruktor der Klasse übergeben werden. Da beim Entwurf der Klasse nicht bekannt ist, wie viele Daten in einem Objekt ablegt werden, wird ein variadischer Konstruktor eingesetzt. Dieser ermittelt mithilfe von sizeof...() aus dem parameter pack die Anzahl der Daten und legt ein entsprechend großes Feld an. Anschließend werden die Daten mittels einer fold expression in das Feld übernommen.
Template-Spezialisierungen
Mithilfe der Template-Spezialisierung kann ein Template für einen bestimmten Datentyp überschrieben werden. So dient z.B. das nachfolgende Klassentemplate Store zum Abspeichern eines beliebigen Datum. Zu diesem Datum kann mit der Methode Add() ein weiteres Datum addiert werden.
#include <print>
template <typename T>
class Store
{
T data; // bel. Datum
public:
// ctor
Store(T _data): data(_data)
{ }
// Addiert toAdd zum Datum
void Add(const T& toAdd)
{
data += toAdd;
}
// Liefert Datum zurueck
const T& GetData() const
{
return data;
}
};
int main()
{
Store intObj{10}; // int-Datum ablegen
Store floatObj{3.14f}; // float-Datum ablegen
// Daten ausgeben
std::println("intObj: {}, floatObj: {}",
intObj.GetData(), floatObj.GetData());
// Zu den Daten Werte addieren
intObj.Add(10);
floatObj.Add(3.14f);
// und Daten ausgeben
std::println("intObj: {}, floatObj: {}",
intObj.GetData(), floatObj.GetData());
}
intObj: 10, floatObj: 3.14
intObj: 20, floatObj: 6.28
Werden Objekte von diesem Klassentemplate für numerische Daten definiert, ist alles in Ordnung. Wird aber ein Objekt definiert, das einen C-String (char Zeiger) enthält, liefert die Methode Add() einen Fehler beim Übersetzen. In einem solchen Fall, in dem einige Template-Methoden des allgemeinen Templates nicht zum Datentyp des instanziierten Objekts passen, kann das Klassentemplate spezialisiert werden.
Um ein Klassentemplate für einen Datentyp zu spezialisieren, bleibt die spitze Klammer in der template-Anweisung des spezialisierten Templates leer. Der Datentyp, für den das Klassentemplate spezialisiert werden soll, wird nach dem Klassennamen in spitzen Klammern angegeben. Dieses spezialisierte Klassentemplate hat nun außer dem Namen mit dem allgemeinen Klassentemplate nichts gemeinsam, d.h., es muss nicht die gleichen Eigenschaften und Methoden wie das allgemeine Klassentemplate besitzen.
Die Definition des für den Datentyp char* spezialisierten Klassentemplates Store würde damit wie folgt aussehen:
// Spezialisiertes Template fuer char-Zeiger
template <>
class Store<const char*>
{ ...};
// Methode des spezialisierten Templates
const char* Store<const char*>::GetData() const
{...};
int main()
{
// Store-Objekt fuer const char* definieren
Store cObj{"Wasdasd"};
...
}
Beachten Sie, dass dann bei der Definition einer Methode außerhalb der Klasse ebenfalls der spezialisierte Datentyp mit angegeben werden muss.
Sind nur eine oder zwei Methoden des allgemeinen Templates für einen bestimmten Datentyp nicht geeignet, können diese alternativ durch Template-Methoden überschrieben werden.
Übung:
tspez_01:
Erstellen Sie ein Klassentemplate Sum zum Aufsummieren von Daten. Die Methode Add() erhält als Parameter das zu addierende Datum und die Methode GetSum() liefert die Gesamtsumme zurück.
Erstellen Sie dann ein spezialisiertes Klassentemplate, um auch C-Strings (char*) zu addieren, sprich aneinanderzuhängen.
Definieren Sie in ein Sum-Objekt zur Addition von int-Werten. Addieren Sie die Werte 0...4 auf und geben Sie die Summe aus.
Definieren Sie in zweites Sum-Objekt zur Addition von C-Strings. Addieren Sie 3 C-Strings und geben die 'Summe' aus
int-Summe (0..4): 10
Addition von eins,zwei,drei,
char*-Summe: eins,zwei,drei,
Partielle Template-Spezialisierung
Besitzt ein Klassentemplate mehrere Template-Parameter, kann für jeden dieser Template-Parameter das Template spezialisiert werden. Bei dieser partiellen Template-Spezialisierung bleibt aber mindestens ein formaler Template-Parameter erhalten.
Die partielle Template-Spezialisierung erfolgt in der Art, dass bei der Template-Definition in der template-Anweisung nur die formalen Datentypen aufgeführt werden. Anschließend werden nach dem Klassennamen in spitzen Klammern die formalen Datentypen und die Datentypen, für die das Klassentemplate spezialisiert werden soll, aufgelistet. Im nachfolgenden Beispiel wird das Klassentemplate CAny für den Fall spezialisiert, dass der Datentyp des zweiten Template-Arguments vom Typ const char* ist. Der Datentyp des ersten Template-Arguments kann weiterhin beliebig sein.
#include <print>
#include <charconv>
#include <exception>
// Allgemeines Klassentemplate
template <typename T1, typename T2>
class CAny
{
T1 data;
public:
// ctor
CAny(T1 _data) : data(_data)
{ }
// Gibt akt. Wert plus value zurueck
T1 Add(T2 value)
{
return data + value;
}
};
// Spezialisiertes Template fuer den Fall
// dass ein textueller Wert addiert wird
template <typename T1>
class CAny<T1, const char*>
{
T1 data;
public:
// ctor
CAny(T1 _data) : data(_data)
{ }
// Addiert textuellen Wert
T1 Add(const char* charValue)
{
// Text nach numerisch
T1 value;
auto res = std::from_chars(charValue, charValue + 99, value);
// Bei Konvertierungsfehler, Ausnahme ausloesen
if (res.ec != std::errc())
{
auto errText = std::format("Ungueltiger C-String '{}' in Add()",
charValue);
throw std::invalid_argument{ errText };
}
// akt. Wert plus numerischen Wert
return data + value;
}
};
int main()
{
// Zwei Objekte definieren
// 1. Objekt addiert short+short
// 2. Objekt addiert int+"Textwert"
CAny<short, short> obj1{ 10 };
CAny<int, const char*> obj2{ 100 };
// Ausnahmebehandlung
try
{
std::println("obj1{{10}}+100={}", obj1.Add(100));
std::println("obj2{{100}}+\"33\"={}", obj2.Add("33"));
// Add("Anton") loest eine Ausnahme aus!
std::println("obj2{{100}}+\"33\"={}", obj2.Add("Anton"));
}
catch (std::exception& ex)
{
std::println("FEHLER! {}", ex.what());
}
}
obj1{10}+100=110
obj2{100}+"33"=133
FEHLER! Ungueltiger C-String 'Anton' in Add()
Übung:
tpspez_01:
Erstellen Sie ein Klassentemplate zum Abspeichern von zwei Daten mit beliebigem Datentyp. Überladen Sie den Operator << für die Ausgabe der Daten.
Für den Fall, dass der zweite Template-Parameter ein char-Zeiger ist, ist das Klassentemplate zu spezialisieren. In diesem Fall ist der über den char-Zeiger referenzierte Text abzulegen und nicht der char-Zeiger. Überladen Sie ebenfalls den Operator << zur Ausgabe der Daten.
Definieren Sie jeweils ein Objekt um einen int- und einen float-Wert abzuspeichern.
Definieren Sie ein zweites Objekt um einen short-Wert und den über den char-Zeiger referenzierten String abzuspeichern.
Geben Sie beide Objekte aus.
Datum1: 10, Datum2: 3.14
Datum1: 42, Datum2: ist die Zahl!
Partielle Template-Spezialisierung non-type Parameter
Außer für Datentypen lassen sich Templates für non-type Template-Parameter spezialisieren. Solche Spezialisierungen werden zum Beispiel eingesetzt, wenn für bestimmte Werte oder Datentypen der im allgemeinen Klassentemplate verwendete Algorithmus vereinfacht werden kann, um die Effizienz des Programms zu erhöhen.
Um ein Klassentemplate für einen bestimmten Wert eines non-type Parameters zu spezialisieren, werden in der template-Anweisung nur die formalen Template-Parameter angegeben. Zusätzlich sind nach dem Klassennamen in spitzen Klammern die Template-Argumente sowie der Wert des non-type Parameters anzugeben, für den das spezialisierte Klassentemplate angewandt werden soll.
Das nachfolgende Beispiel spezialisiert ein Template StoreData für den Fall, dass bis zu 10 Elemente abgespeichert werden sollen. Für bis zu 10 Elemente wird das Feld statisch angelegt und im anderen Fall (mehr als 10 Elemente) dynamisch.
Das Template besitzt 2 non-type Parameter, SIZE und BIG. SIZE ist Größe des übergebenen Feldes und wird vom Compiler automatisch berechnet. Der zweite non-type Parameter BIG ist ist abhängig von SIZE und ist nur dann true, wenn SIZE>10 ist und im anderen Fall false. Und für den Wert false wird das spezialisierte Template instanziiert.
#include <print>
// Allg. Template fuer Felder mit mehr als 10 Elemente
// Der non-type Parameter BIG ist true, wenn SIZE>10
template <typename T, int SIZE, bool BIG=(SIZE>10)>
class StoreData
{
T* data;
public:
// ctor, legt Feld dynamisch an
StoreData(T(&_data)[SIZE])
{
data = new T[SIZE];
for (int index = 0; index < SIZE; index++)
data[index] = _data[index];
}
~StoreData()
{
delete[] data;
std::println("Feld geloescht!");
}
void Print()
{
std::println("Dynamisches Feld:");
for (int index = 0; index < SIZE; index++)
std::print("{}, ", data[index]);
std::println("");
}
};
// Spezialisiertes Template fuer mit bis zu 10 Elemente
// Spezialisierung erfolgt ueber den non-type Parameter
// BIG gleich false
template <typename T, int SIZE>
class StoreData<T, SIZE, false>
{
T data[SIZE]; // statische Feld
public:
StoreData(T(&_data)[])
{
for (int index = 0; index < SIZE; index++)
data[index] = _data[index];
}
void Print()
{
std::println("Statisches Feld:");
for (int index = 0; index < SIZE; index++)
std:: print("{}, ", data[index]);
std::println("");
}
};
int main()
{
// Kleines Feld mit 4 Elemente abspeichern
int iArray[]{ 1,2,3,4 };
StoreData obj1{ iArray };
obj1.Print();
// Grosses Feld mit 12 Elemente abspeichern
float var[12]{};
StoreData obj2{ var };
obj2.Print();
}
Statisches Feld:
1, 2, 3, 4,
Dynamisches Feld:
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
Feld geloescht!
Enthält eine Template-Definition zwei non-type Parameter, kann zudem das Klassentemplate für den Fall spezialisiert werden, dass beide non-type Parameter den gleichen Wert besitzen, und dies unabhängig vom Wert.
So wird im nachfolgenden Beispiel CAny für den Fall spezialisiert, dass der Template-Parameter ROW den gleichen Wert besitzt wie COL. Dabei ist zu beachten, dass innerhalb der Template-Definition nur ein non-type Parameter steht, aber nach dem Klassennamen in der spitzen Klammer beide non-type Argumente anzugeben sind.
// Klassentemplate für unterschiedliche ROW / COL-Werte
template <typename T, int ROW, int COL>
class CAny
{...};
// Partiell spezialisiertes Klassentemplate
// für den Fall, dass ROW gleich COL ist
template <typename T, int SIZE>
class CAny<T,SIZE,SIZE>
{...};
// Objekte Definitionen
CAny<short,3,5> obj1; // Allg. Klassentemplate
CAny<float,4,4> obj2; // Spezialisiertes Klassentemplate
Und zu guter Letzt kann ein Klassentemplate auch für bestimmte Werte eines non-type Parameters spezialisiert werden. Das nachfolgende Klassentemplate Image soll zum Zeichnen von Grafiken dienen. Für den Fall, dass eine Grafik mit der Größe 32x32 gezeichnet werden soll, könnte z.B. ein optimierter Algorithmus zum Zeichnen der Grafik zum Einsatz kommen. Beachten Sie, dass die Template-Definition des spezialisierten Klassentemplates nun leer ist.
// Allgemeines Klassentemplate
template <int WIDTH, int HEIGHT>
class Image
{...};
// Partiell spez. Klassen-Tpl für den Fall
// WIDHT = HEIGHT = 32
template <>
class Image<32,32>
{...};
// Objekte Definitionen
Image<640,480> obj1; // Allgemeines Klassentemplate
Image<32,32> obj2; // Spezialisiertes Klassentemplate
Übung:
tspezn_01:
Erstellen Sie ein Klassentemplate Matrix zur Bearbeitung von Matrizen. Die Klasse soll Methoden zum Setzen eines Matrix-Elements, zur Ausgabe der Matrix und zum Transponieren der Matrix enthalten.
Das Transponieren einer Matrix erfolgt in der Form, dass die Spalten der Ausgangsmatrix zu Zeilen der Zielmatrix werden und umgekehrt. D. h., aus einer 3x4 Matrix wird nach dem Transponieren eine 4x3 Matrix.
Sollen quadratischen Matrizen, Zeilenanzahl gleich Spaltenanzahl, transponiert werden, lässt sich dieser Algorithmus wesentlich vereinfachen. Zum einen muss kein neues 2-dimensionales Feld reserviert werden, da die Größe der transponierten Matrix identisch mit der Größe der Ausgangsmatrix ist. Und zum anderen lässt sich der Kopiervorgang verkürzen. Aus diesem Grund ist das Klassentemplate Matrix für den Fall einer quadratischen Matrix zu spezialisieren.
Definieren Sie eine 3x4 Matrix und initialisieren sie mit beliebigen Daten. Geben Sie die Ausgangsmatrix und die transponierte Matrix jeweils aus.
Definieren Sie eine quadratischen 4x4 Matrix und initialisieren sie mit beliebigen Daten. Geben Sie die Ausgangsmatrix und die transponierte Matrix jeweils aus.
Ausgangsmatrix:
11 21 31 41
12 22 32 42
13 23 33 43
Transponierte Matrix:
11 12 13
21 22 23
31 32 33
41 42 43
Ausgangsmatrix:
10.1 10.2 10.3 10.4
20.1 20.2 20.3 20.4
30.1 30.2 30.3 30.4
40.1 40.2 40.3 40.4
Transponierte Matrix:
10.1 20.1 30.1 40.1
10.2 20.2 30.2 40.2
10.3 20.3 30.3 30.4
10.4 20.4 40.3 40.4
Template-Template Parameter (TTP)
Wie der Name sagt, sind TTPs Parameter von Templates, die wiederum Templates sind. Sehen wir uns das Einsatzgebiet von TTPs anhand eines praktischen Beispiels an.
Vorgegeben sei das Klassentemplate Store zum Abspeichern von Daten. Der für die Daten erforderliche Speicherplatz wird, je nach Zustand des Template-Parameters newAlloc, entweder mittels des new Operators oder mithilfe der Bibliotheksfunktion malloc() reserviert.
// Klassentemplate
template <typename T, bool newAlloc>
class Store
{
T* data;
public:
Store(...)
{
if (newAlloc)
data = new T;
else
data = static_cast<T*>(malloc(sizeof T));
...
}
...
};
// Objekt Definitionen
Store<int,true> intNew;
Store<float,false> floatMalloc
Da die beiden verwendeten Verfahren zur Reservierung von Speicher noch an anderen Stellen eingesetzt werden sollen, werden sie aus dem ursprünglichen Klassentemplate herausgelöst und als eigenständige Klassentemplates definiert.
// Klassentemplates zum Reservieren von Speicher
template <typename T>
struct NewAlloc // Reserviert mit new
{...};
template <typename T>
struct MallocAlloc // Reserviert mit malloc()
{...};
// Klassentemplate für Daten
template <typename T, ???>
class Store
{...};
// Objekt Definitionen
Store<int,???> intNew;
Store<float,???> floatMalloc;
Bleibt damit die Frage, wie das Klassentemplate NewAlloc bzw. MallocAlloc für die Reservierung des Speichers an das Klassentemplate Store übergeben wird und wie Objekte vom Typ Store definiert werden.
Beginnen wir mit der template-Anweisung eines Templates, das als zweiten Parameter einen TTP besitzt. Da der Template-Parameter selbst ein Template ist, ist dies bei der template-Anweisung wie nachfolgend dargestellt anzugeben.
// Klassentemplate für Daten
template <typename T, template <typename> class TAlloc>
class Store
{...};
Und hier gilt zu beachten, dass beim <typename> des TTPs kein formaler Datentyp für den einzusetzenden Datentyp steht.
Wenn wir weiter davon ausgehen, dass die beiden Klassentemplates NewAlloc und MallocAlloc eine Methode Create() enthalten um den Speicher zu reservieren, kann Create() wie angegeben aufgerufen werden. Der Platzhalter T gibt hier den Datentyp an, für den Speicher zu reservieren ist.
// Klassentemplate für Daten
<template <typename T, template <typename> class TAlloc>
class Store
{
T* data;
public:
Store(...)
{
// Speicher reservieren!
data = TAlloc<T>::Create();
}
...
};
Für den TTP kann ebenfalls ein Default-Klassentemplate vorgegeben werden. Dazu wird innerhalb der template-Anweisung nach dem Namen des TTPs der Zuweisungsoperator gefolgt vom Namen des Default-Templates angegeben.
// Klassentemplate für Daten
template <typename T, template <typename> class TAlloc=NewAlloc>
class Store
{...};
Bei der Definition eines Objekts eines Klassentemplates mit einem TTP ist das als TTP zu verwendende Klassentemplate innerhalb der spitzen Klammern anzugeben. Besitzt das Klassentemplate einen Default-TTP, kann diese Angabe selbstverständlich entfallen.
// Objekt Definitionen
Store<int, NewAlloc> newInt;
Store<Demo, MallocAlloc> mallocObj;
Store<float> newFloat; // Verwendet Default-Template
Im Beispiel werden drei Objekte des Klassentemplates Store definiert. Das erste Objekt newInt speichert int-Werte ab und die Speicherreservierung erfolgt über das Klassentemplate NewAlloc. Das zweite Objekt mallocObj speichert Objekte der Klasse Demo ab, deren Speicher über das Klassentemplate MallocAlloc reserviert wird. Und zu guter Letzt verwendet das Objekt newFloat das Default-Klassentemplate für die Speicherreservierung, um float-Werte abzulegen.
Gingen die bisherigen Beispiele immer davon aus, dass das Klassentemplate und das TTP-Template den gleichen Datentyp für den formalen Datentyp verwenden, muss dies nicht zwangsläufig so sein. Im Beispiel unten verwendet das Klassentemplate CAny den formalen Datentyp T1 und der TPP den formalen Datentyp T2. Wird Objekt vom Typ CAny definiert, sind beide Datentypen, der für das Klassentemplate und der für den TTP, zu spezifizieren.
// Klassentemplate mit TTP
template <typename T1, typename T2,
template <typename> class TP>
class CAny
{
T1 data;
void DoAnything()
{
// Methode des TTPs aufrufen
TP<T2>::DoSomething();
}
...
};
// Objekt Definition
CAny<short, float, TTPClass> obj;
Übung:
ttp_01:
Erstellen Sie zwei Klassentemplates zur dynamischen Reservierung von Speicher. Das Klassentemplate NewAlloc soll hierfür den new Operator verwenden und das Klassentemplate MallocAlloc die malloc() Funktion. Für beide Klassentemplates sind die Methoden Create() und Release() zu erstellen, um Speicher zu reservieren bzw. wieder freizugeben.
Die Funktion malloc() ist wie folgt deklariert:
void* malloc (std::size_t size);
Der Parameter size enthält die Anzahl der Bytes, für die Speicher reserviert werden soll. Als Rückgabewert liefert die Funktion einen void-Zeiger auf den reservierten Speicher.
Eine 'kleine' Besonderheit ist zu beachten, wenn über das Klassentemplate MallocAlloc Speicher für Objekte reserviert wird. Da malloc() nur den Speicher anfordert und nicht wie der new Operator automatisch den Konstruktor des Objekts aufruft, muss der Konstruktoraufruf explizit erfolgen. Der Aufruf des Konstruktors erfolgt über den sogenannten placement-new-Operator.
T* res = new(memAdr) obj;
Der placement-new-Operator reserviert selbst keinen Speicher, sondern initialisiert nur das an ihn übergebene obj Objekt und liefert einen Zeiger auf das initialisierte Objekt zurück. Die Speicheradresse des Objekts memAdr (vorher von malloc() zurückgeliefert) wird an den placement-new Operator übergeben.
Ebenso ist beim Löschen des Objekts der Destruktor explizit aufzurufen. Und erst nach dem Aufruf des Destruktors darf der Speicher freigegeben werden!
memAdr->~T();
free(memAdr);
Erstellen Sie ein weiteres Klassentemplate StoreData zum Abspeichern eines beliebigen Datums innerhalb eines Moduls. Der Speicher für das abzulegende Datum soll entweder über NewAlloc oder MallocAlloc reserviert werden. Standardmäßig ist der Speicher mittels NewAlloc zu reservieren. Zusätzlich sind die beiden Methoden SetData() zum Setzen des Datums und GetData() zum Auslesen des Datums zu implementieren.
Ziel ist es, das folgendes Programm ausführen zu können:
#include <print>
#include <iostream>
#include <string>
// Datei fuer die Speicherreservierung
#include "ttp_01alloc.h"
// Modul zum Abspeichern eines bel. Datums
import StoreData;
// Demo-Objekt
import CData3;
// main() Funktion
int main()
{
// Platz fuer int-Wert mit malloc anlegen
std::println("int mit MallocAlloc:");
StoreData<int, MallocAlloc> intParam;
intParam.SetData(10);
std::println("Datum: {}", intParam.GetData());
// Platz fuer string-Wert mit new anlegen
std::println("\nstring mit NewAlloc:");
StoreData<std::string, NewAlloc> stringParam;
stringParam.SetData("Another string");
std::println("Datum: {}", stringParam.GetData());
// Platz fuer float-Wert mit Default-Allokierung anlegen
std::println("\nfloat mit Default-Template:");
StoreData<float> floatParam;
floatParam.SetData(3.1416f);
std::println("Datum: {}", floatParam.GetData());
// Platz fuer CData3-Objekt mit malloc anlegen
std::println("\nCData3 mit MallocAlloc:");
StoreData<CData3, MallocAlloc> classParam;
std::cout << "Datum: " << classParam.GetData() << '\n';
classParam.SetData(CData3("A new string"));
std::cout << "Datum: " << classParam.GetData() << '\n';
// Ab hier wird wieder aufgeraeumt
std::println("\nUnd alles wieder entfernen:");
}
int mit MallocAlloc:
MallocAlloc::Create(#0x25adbba9290)
Datum: 10
string mit NewAlloc:
NewAlloc::Create(#0x25adbbb05e0)
Datum: Another string
float mit Default-Template:
NewAlloc::Create(#0x25adbba92b0)
Datum: 3.1416
CData3 mit MallocAlloc:
MallocAlloc::Create(#0x25adbbb0370)
Datum:
Datum: A new string
Und alles wieder entfernen:
MallocAlloc::Release(#0x25adbbb0370)
NewAlloc::Release(#0x25adbba92b0)
NewAlloc::Release(#0x25adbbb05e0)
MallocAlloc::Release(#0x25adbba9290)
Anmerkung: Zur besseren Verfolgung der Aufrufe von Create() und Release() geben die Methoden ihre Namen sowie die Anfangsadresse des reservierten Speicher im Beispiel aus.
std::format() und std::print() für eigene Datentypen
Dieser Abschnitt behandelt keine neuen Template-Eigenschaften, sondern zeigt wie Templates in der Praxis eingesetzt werden.
Bisher konnten Objekte von eigenen Klasse nur durch Überladen des Operators << ausgegeben werden. Durch Spezialisierung des in der Standardbiblithek definierten Templates formatter ist es jedoch möglich, Objekte von eigenen Klassen mit print() auszugeben bzw. mit format() für die Ausgabe zu formatieren.
Sehen wir uns an, wie für eine Klasse Complex das spezialisierte Template zu definieren ist, um ein Objekt dieses Typs auszugeben.
#include <print>
class Complex
{
float real;
float imag;
public:
Complex(float r, float i) : real(r), imag(i)
{}
};
int main()
{
Complex obj(1.2f, 3.4f);
// Das ist die gewuenschte Ausgabe
std::println("Complex: {}", obj);
}
Als Erstes sind im spezialisierten Template formatter die Methoden parse() und format() wie folgt zu definieren:
// Spezialisertes formatter-Template fuer
// den Datentyp Complex
template<>
struct std::formatter<Complex>
{
constexpr auto parse(auto& ctx)
{
// Formatspezifikation analysieren
}
auto format(const Complex& s,
std::format_context& ctx) const
{
// format_to() zur formatierten Ausgabe aufrufen
}
};
Die Methode parse() dient zur Auswertung der Formatspezifikation (das ist alles was innerhalb der {} Klammern von print() bzw. format() steht). Der Parameter ctx verweist auf ein Kontext-Objekt, dessen Methode begin() auf das erste Zeichen in der Formatspezifikation zeigt. Da wir im ersten Ansatz keine Formatspezifikation verwenden, sondern nur einen leeren Platzhalter {}, gibt die Methode einfach den Verweis wieder zurück.
Die Ausgabe der Eigenschaften des Complex-Objekts erfolgt in der Methode format(). Dazu erhält die Methode im ersten Parameter eine const-Referenz auf das auszugebende Objekt und im zweiten Parameter ctx eine Referenz auf einen Formatierungskontext. Die Formatierung der Ausgabe erfolgt über die Bibliotheksfunktion format_to(), die im ersten Parameter einen Verweis auf den Ausgabepuffer des Formatierungskontextes erhält. Danach folgen der Formatierungsstring, mit den bekannten Formatierungsoptionen der format() Bibliotheksfunktion, sowie die zu formatierenden Daten.
Als Rückgabewert muss die Methode format() einen Verweis auf das Ende der formatierten Ausgabe liefern, den die Funktion format_to() zurückgibt.
Das vollständige Programm könnte damit wie folgt aussehen:
#include <print>
class Complex
{
float real;
float imag;
public:
Complex(float r, float i) : real(r), imag(i)
{}
// friend-Deklaration erforderlich, damit das
// Template auf die Eigenschaften zugreifen kann
friend struct std::formatter<Complex>;
};
// Spezialisertes formatter-Template fuer
// den Datentyp Complex
template<>
struct std::formatter<Complex>
{
constexpr auto parse(auto& ctx)
{
// Formatspezifikation analysieren
// hier noch nicht erforderlich
return ctx.begin();
}
auto format(const Complex& s,
std::format_context& ctx) const
{
// format_to() zur formatierten Ausgabe aufrufen
// Formatierte Daten in Puffer den ctx.out() liefert
return std::format_to(ctx.out(),
"{:.2f}r+{:.2f}i ", s.real, s.imag);
}
};
int main()
{
Complex obj(1.2f, 3.4f);
// Das ist die gewuenschte Ausgabe
std::println("Complex: {}", obj);
}
Complex: 1.20r+3.40i
Beachten Sie, dass das spezialisierte Template ein friend-Template der Klasse sein muss, wenn auf dessen private-Eigenschaften zugegriffen wird.
Soll das Format für die Ausgabe eines Objekts durch Formatspezifizierer gesteuert werden, erfolgt die Ausgabe '2-stufig'. Zuerst sind die Formatspezifizierer in der Methode parse() auszuwerten und anschliesend ist die Ausgabe in format() entsprechend zu formatieren.
Für unsere Beispielklasse sollen z.B. folgende Ausgaben möglich sein:
Complex: 1.2r+3.4i (Ausgabe Real- und Imaginäranteil)
Complex: 1.2r (Ausgabe nur Realanteil)
Complex: 3.4i (Ausgabe nur Imaginäranteil)
Die Ausgabe soll dabei über die Formatspezifizierer {:c}, {:r} und {:i} gesteuert werden.
Sehen wir uns die vollständige Auswertung der Formatspezifizierer an:
#include <print>
class Complex
{
float real;
float imag;
public:
Complex(float r, float i) : real(r), imag(i)
{}
// friend-Deklaration erforderlich, damit das
// Template auf die Eigenschaften zugreifen kann
friend struct std::formatter<Complex>;
};
// Spezialisertes formatter-Template fuer
// den Datentyp Complex
template <>
struct std::formatter<Complex>
{
private:
// Formatoptionen
enum class ftype { REAL, IMAG, COMPLETE }
outType = ftype::COMPLETE;
public:
constexpr auto parse(auto& ctx)
{
// Verweise auf Beginn und Ende der Formatoption
auto iter{ ctx.begin() };
const auto end{ ctx.end() };
// Wenn kein Formatspezifierer angegeben
if ((iter == end) || (*iter == '}'))
{ // Alles ausgegeben
outType = ftype::COMPLETE;
return iter;
}
switch (*iter) // Formatierungsoption auswerten
{ // und abspeichern
case 'i':
outType = ftype::IMAG;
break;
case 'r':
outType = ftype::REAL;
break;
case 'c':
outType = ftype::COMPLETE;
break;
default:
throw std::format_error{ "Ungueltiges Format!" };
}
// Naechstes Zeichen muss eine } sein
++iter;
if ((iter != end) && (*iter != '}'))
throw std::format_error{ "Ungueltiges Format!" };
return iter;
}
auto format(const Complex& s, std::format_context& ctx) const
{
// Formatierte Daten in Puffer
switch (outType)
{
using enum ftype;
case COMPLETE:
return std::format_to(ctx.out(),
"{:.2f}r+{:.2f}i", s.real, s.imag);
case REAL:
return std::format_to(ctx.out(), "{:.2f}r", s.real);
case IMAG:
return std::format_to(ctx.out(), "{:.2f}i", s.imag);
}
}
};
int main()
{
Complex obj(1.2f, 3.4f);
// Das ist die gewuenschte Ausgabe
std::println("Complex std : {}", obj);
std::println("Complex real: {:r}", obj);
std::println("Complex imag: {:i}", obj);
std::println("Complex r+i : {:c}", obj);
}
Complex std : 1.20r+3.40i
Complex real: 1.20r
Complex imag: 3.40i
Complex r+i : 1.20r+3.40i
Der Ablauf in parse() ist relativ einfach. In Zeile 31 wird zunächst geprüft, ob ein leerer Platzhalter im Formatstring angegeben wurde. Ist dies der Fall, wird das Flag outType 'COMPLETE' gesetzt. Im anderen Fall wird mittels der switch-Anweisung der Formatspezifizierer ausgewertet und das Flag outType entsprechend gesetzt. Bei einem ungültigen Formatspezifizierer wird im default-Zweig der switch-Anweisung eine Ausnahme ausgelöst. Zum Schluss wird in Zeile 52 noch geprüft, ob nach dem Formatspezifizierer die geschweifte Klammer zu richtig gesetzt ist.
Jetzt muss nur noch die in parse() ermittelte Ausgabeformatierung durch Auswerten des Flags outType in der Methode format() entsprechend umgesetzt.
Übung:
tformat_01:
Definieren Sie eine Klasse Person mit den Eigenschaften name und age.
Erstellen Sie ein spezialisiertes formatter-Template, um ein Objekt vom Typ Person wie folgt auszugeben:
std::println("Name & Alter: {}",obj);
std::println("Name : {:n}",obj);
std::println("Alter: {:a}",obj);
Wird kein Formatspezifizierer angegeben, sind der Name und das Alter auszugeben. Wird der Formatspezifizierer n angegeben, wird nur der Namen ausgegeben und beim Formatspezifizierer a nur das Alter.
Definieren Sie in ein Objekt vom Typ Person und geben dessen Eigenschaften wie oben angegeben aus.
Name und Alter: Max Mustermann, 45
Name : Max Mustermann
Alter: 45 Jahre