C++ Tutorial

Template-Spezialitäten

Template-Objekte als Parameter

Bei der Übergabe von Klassentemplate-Objekte an eine Methode/Funktion ist diese ebenfalls als Template zu definieren. Dabei sind zwei Fälle zu unterscheiden:

Funktion erhält immer Objekte des gleichen Templates

Als Datentyp des Parameters ist 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.

1: // Klassentemplate
2: template <typename T>
3: class MyClass
4: {
5:    T GetData();
6:    ...
7: };
8: // Funktionstemplate mit Template als Parameter
9: template <typename T>
10: void DoAny(MyClass<T>& obj)
11: {
12:    ...
13:    T val = obj.GetData();
14: }
15:
16: MyClass<int> intClass;
17: DoAny(intClass);     // T in DoAny(): int
18: MyClass<float> floatClass;
19: DoAny(floatClass);   // T in DoAny(): float

Funktion erhält Objekte unterschiedlicher Templates

In diesem Fall ist als Datentyp des Funktionsparameters der formale Datentyp anzugeben. Der formale Datentyp des Parameters ist nun vom Typ des übergebenen Klassentemplates.

Bleibt ein kleines Problem bestehen, das es zu lösen gilt, wenn aus dem Funktionstemplate 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>
// Klassentemplate 1
template <typename T>
class ClassOne
{
   T prop;
public:
   ClassOne(T val) : prop{ val }
   {}
   T GetProp() const
   {
      return prop;
   }
};
// Klassentemplate 2
template <typename T>
class ClassTwo
{
   T prop;
public:
   ClassTwo(T val) : prop{ val }
   {}
   T GetProp() const
   {
      return prop;
   }
};
// Funktion erhaelt als Parameter ein Objekt
// eines beliebigen Klassentemplates und ruft
// dessen Methode GetProp() auf
template <typename T>
auto GetValue(T& obj)
{
   // Datentyp von retVal wird automatisch von
   // Returntyp der aufgerufenen Methode bestimmt
   auto retVal = obj.GetProp();
   std::println("GetValue returned {}", typeid(retVal).name());
   return retVal;
}
int main()
{
   // Zwei Objekte mit unterschiedlichen
   // Klassentemplates definieren
   ClassOne obj1{ 1 };
   ClassTwo obj2{ 2.2f };
   // GetValue mit Klassentemplate-Objekt aufrufen
   std::println("obj1: {}", GetValue(obj1));
   std::println("obj2: {}", GetValue(obj2));
}

GetValue returned int
obj1: 1
GetValue returned float
obj2: 2.2

Klassentemplates und Ableitungen

Beim Ableiten von Klassentemplates können drei Fälle auftreten:

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 Datentyp stets derselbe, kann die abzuleitende Klasse als normale Klasse definiert werden (erste Ableitung MyClass). Variiert der Datentyp, ist die abzuleitende Klasse ebenfalls als Klassentemplate zu definieren, wobei das Template-Argument an die Basisklasse weitergegeben wird (zweite Ableitung YourClass).

1: // Die Basisklasse
2: template <typename T>
3: class CAny
4: {...};
5:
6: // Abgeleitete Klassen
7: class MyClass: public CAny<int>
8: {...};
9:
10: template <typename T>
11: class YourClass: public CAny<T>
12: {...};
13: 14: // Objekte definieren
15: CAny<flat> obj1; // Basisklassen-Objekt
16: MyClass obj2; // Basisklasse: CAny<int>
17: YourClass<short> obj3; // Basisklasse: CAny<short>

Basisklasse wird über formalen Datentyp definiert

Hierbei ist die abzuleitende Klasse immer als Klassentemplate zu definieren. Bei der Ableitung ist als Basisklasse der formale Datentyp anzugeben.

1: // Zwei Basisklassen
2: class CAny
3: {...};
4: class Some
5: {...};
6:
7: // Abgeleitete Klasse,
8: // Basisklasse wird über Template-Argument festgelegt
9: template <typename T>
10: class MyClass: public T
11: {...};
12:
13: // Objekte definieren
14: MyClass<CAny> anyBase;    // Basisklasse CAny
15: MyClass<Some> someBase;   // Basisklasse Some

Basisklasse ist ein weiteres Klassentemplate

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.

1: // Klassentemplates als Basisklassen
2: template <typename T>
3: class Base1
4: {...};
5: template <typename T>
6: class Base2
7: {...};
8:
9: // Abgeleitete Klasse
10: template <typename T>
11: class Der: public T
12: {...};
13:
14: // Objekte definieren
15: Der<Base1<short>> obj1;
16: Der<Base2<float>> obj2;

Membertemplates

Außer dass Templates für Klassen definiert werden können, können Methoden innerhalb einer 'normalen' Klasse als Templates definiert werden. Diese Templates werden als Membertemplates bezeichnet. Mithilfe von Membertemplates kann unter Umständen unnötiger Code vermieden werden, da nicht mehr unzählige überladene Methoden im Voraus definiert werden müssen.

Innerhalb der Klasse wird ein Membertemplate auf die gleiche Weise wie ein Funktionstemplate definiert. Die Klasse, die das Membertemplate enthält, wird als 'normale' Klasse definiert. Die Definition eines Objekts erfolgt wie gewohnt, d.h. ohne Angabe eines Template-Arguments.

1: class CAny
2: {
3:    ...
4: public:
5:    // Membertemplate
6:    template <typename T>
7:    void Write (const T& val)
8:    {...}
9: };
10: // Objekt definieren
11 : CAny obj;
12: // Aufruf erzeugt entsprechende Methode
13: obj.Write(10);     // erzeugt CAny::Write(const int&)
14: obj.Write(3.1);    // erzeugt CAny::Write(const double&)

Wird das Membertemplate nicht innerhalb der Klasse, sondern außerhalb definiert, ist der Methode eine template-Anweisung voranzustellen. Hierbei ist zu beachten, dass nach dem Klassennamen kein Templateparameter angegeben wird.

1: class CAny
2: {
3:    ...
4: public:
5:    // Membertemplate deklarieren
6:    template <typename T>
7:    void Write (const T& val);
8: };
9: // Membertemplate ausserhalb definieren
10: template <typename T>
11: void CAny::Write(const T& val)
12: {...}

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. Falls für das Klassentemplate und das Membertemplate die gleichen formalen Datentypen verwendet werden, verdeckt der Bezeichner des Membertemplates den Bezeichner des Klassentemplates.

1: template <typename T1>
2: class CAny
3: {
4:    ...
5: public:
6:    // Membertemplate
7:    template <typename T2>
8:    void Write (const T2& val)
9:    {...}
10: };
11: // Objekt definieren
12: CAny<short> obj;
13: // Aufruf erzeugt entsprechende Methode
14: obj.Write(10);    // erzeugt: Write(const int&)
15: obj.Write(3.1);   // erzeugt: Write(const double&)

Etwas komplizierter sieht die Sache aus, 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.

1: template <typename T1>
2: class CAny
3: {
4:    ...
5: public:
6:    // Membertemplate deklarieren
7:    template <typename T2>
8:    void Write (const T2& val);
9: };
10: // Membertemplate ausserhalb definieren
11: template <typename T1>
12:   template <typename T2>
13: void CAny<T1>::Write(const T2& val)
14: {...}

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 <iostream>
#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::cout << pData[i] << '\n';
      std::cout << '\n';
   }
};
int main()
{
   // Feld mit int-Daten anlegen und ausgeben
   VArray<int> intArray{1,2,3,4};
   intArray.Print();
   // Feld mit strings anlegen und ausgeben
   VArray<std::string> sArray {"eins"s, "zwei"s, "drei"s};
   sArray.Print();
}

Das Klassentemplate VArray dient zur Ablage von beliebigen Elementen in einem Feld, die in einer Liste an den Konstruktor der Klasse übergeben werden. Da beim Entwurf der Klasse nicht bekannt ist, wie viele Elemente in einem Objekt ablegt werden, wird ein variadischer Konstruktor eingesetzt. Dieser ermittelt mithilfe von sizeof...() aus dem parameter pack die Anzahl der Elemente und legt ein entsprechend großes Feld an. Anschließend werden die Daten mittels einer fold expression in das Feld übernommen.

Template Spezialisierungen

Das nachfolgende Klassentemplate Store dient zur Ablage eines beliebigen Datums. Die Methode Add() erhält als Parameter einen Wert, der zum abgelegten Datum addiert wird.

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.

1: // Klassentemplate
2: template <typename T>
3: class Store
4: {
5:    T data;
6:    ...
7: public:
8:    Add(const T& param)
9:    {
10:      data += param;
11:   }
12: };
13: // Objekte definieren
14: Store<int> intObj;
15: intObj.Add(10);            // int Addition
16: Store<char*> cptrObj;
17: cptrObj.Add("Text hinzu"); // char* Addition

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 Namen des Klassentemplates in spitzen Klammern angegeben. Dieses spezialisierte Klassentemplate hat 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.

1: // allgemeines Klassentemplate
2: template <typename T>
3: class Store
4: {...};
5: // spezialisiertes Klassentemplate fuer char*
6: template <>
7: class Store<char*>
8: {...};
9: ...
10: // Objekte definieren
11: Store<int> intObj;      // allg. Template-Objekt
12: intObj.Add(10);
13: Store<char*> cptrObj;   // char* Template-Objekt
14: cptrObj.Add("huhu");

std::format() und std::print() für eigene Datentypen

Für die Ausgabe von eigenen Datentypen mittels der format() und print() Bibliotheksfunktionen ist das in der Standardbibliothek definierte Template formatter zu spezialisieren.

Sehen wir uns an, wie für eine Klasse Complex das spezialisierte Template zu definieren ist, um ein Objekt dieses Typs auszugeben.

1: class Complex
2: {
3:    float real;
4:    float imag;
5: public:
6:    Complex(float r, float i) : real(r), imag(i)
7:    {}
8: };
9: int main()
10: {
11:    Complex obj(1.2f, 3.4f);
12:    // Das ist die gewuenschte Ausgabe
13:    std::println("Complex: {}", obj);
14: }

Als Erstes sind im spezialisierten Template formatter die Methoden parse() und format() wie folgt zu definieren:

1: // Spezialisertes formatter-Template fuer
2: // den Datentyp Complex
3: template<>
4: struct std::formatter<Complex>
5: {
6:    constexpr auto parse(auto& ctx)
7:    {
8:       // Formatspezifikation analysieren
9:    }
10:   auto format(const Complex& s,
11:               std::format_context& ctx) const
12:   {
13:      // format_to() zur formatierten Ausgabe aufrufen
14:   }
15: };

Die Methode parse() dient zur Auswertung der Formatspezifikation und erhält im Parameter ctx ein Kontext-Objekt, dessen Methode begin() auf das erste Zeichen in der Formatspezifikation verweist. 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 den 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 dann wie folgt aussehen:

#include <print>
class Complex
{
   float real;
   float imag;
public:
   Complex(float r, float i) : real(r), imag(i)
   {}
   // friend Template
   friend struct std::formatter<Complex>;
};
template<>
struct std::formatter<Complex>
{
public:
   constexpr auto parse(auto& ctx)
   {
      return ctx.begin();
   }
   auto format(const Complex& s, std::format_context& ctx) const
   {
      // Formatierte Daten in Puffer
      return std::format_to(ctx.out(), "{:.2f}r+{:.2f}i ",s.real,s.imag);
   }
};
int main()
{
   Complex obj(1.2f, 3.4f);
   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, sind die Formatspezifizierer in der Methode parse() auszuwerten und die Ausgabe ist 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 Auswertung der Formatspezifizierer in der parse() Methode an:

1: template <>
2: struct std::formatter<Complex>
3: {
4: private:
5:    enum class ftype { REAL, IMAG, COMPLETE }
6:    outType = ftype::COMPLETE;
7: public:
8:    constexpr auto parse(auto& ctx)
9:    {
10:      auto iter{ ctx.begin() };
11:      const auto end{ ctx.end() };
12:      if ((iter == end) || (*iter == '}'))
13:      {
14:         outType = ftype::COMPLETE;
15:         return iter;
16:      }
17:      switch (*iter)
18:      {
19:      case 'i':
20:         outType = ftype::IMAG;
21:         break;
22:      case 'r':
23:         outType = ftype::REAL;
24:         break;
25:      case 'c':
26:         outType = ftype::COMPLETE;
27:         break;
28:      default:
29:         throw format_error{ "Ungueltiges Format!" };
30:      }
31:      ++iter;
32:      if ((iter != end) && (*iter != '}'))
33:         throw format_error{ "Ungueltiges Format!" };
34:      return iter;
35:   }
36:   auto format(Complex s, std::format_context& ctx) const
37:   {
38:      ...
39:   }
40: };

Der Ablauf in parse() ist relativ einfach. In Zeile 12 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 32 noch geprüft, ob nach dem Formatspezifizierer die geschweifte Klammer zu richtig gesetzt ist. Die in parse() ermittelte Ausgabeformatierung wird in format() entsprechend umgesetzt.

1: auto format(Complex s, std::format_context& ctx) const
2: {
3:    // Formatierte Daten in Puffer
4:    switch (outType)
5:    {
6:       using enum ftype;
7:    case COMPLETE:
8:       return std::format_to(ctx.out(),
9:                             "{:.2f}r+{:.2f}i", s.real, s.imag);
10:   case REAL:
11:      return std::format_to(ctx.out(), "{:.2f}r", s.real);
12:   case IMAG:
13:      return std::format_to(ctx.out(), "{:.2f}i", s.imag);
14:   }
15: }

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 string ist. Der Datentyp des ersten Template-Arguments kann weiterhin beliebig sein.

1: // Allgemeines Klassentemplate
2: template <typename T1, typename T2>
3: class CAny
4: {
5:    T1 data;
6:    void DoAnything(const T2& data)
7:    {
8:       ...
9:    }
10: };
11: // Partiell spez. Klassentemplate für den Datentyp string
12: template <typename T1>
13: class CAny<T1, std::string>
14: {
15:    T1 data;
16:    void DoAnything(const std::string& data)
17:    {
18:       ...
19:    }
20: };
21: // Objekt Definitionen
22: CAny<int,int> iObj;
23: CAny<float,std::string> sObj;

Partielle Template-Spezialisierung non-type Parameter

Außer für Datentypen lassen sich Templates für non-type Template-Parameter spezialisieren. Weiterhin ist eine Spezialisierung für bestimmte Werte des non-type Parameters möglich. Solche Spezialisierungen werden zum Beispiel eingesetzt, wenn für bestimmte Werte 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.

Im Beispiel wird das Klassentemplate CAny für den Fall spezialisiert, dass der non-type Parameter den Wert 32 besitzt.

1: // Allgemeines Klassentemplate
2: template <typename T, int SIZE>
3: class CAny
4: {...};
5: // Partiell spez. Klassentemplate für den Fall SIZE=32
6: template <typename T>
7: class CAny<T,32>
8: {...};
9: // Objekte Definitionen
10: CAny<short,5> obj1;     // Allg. Klassentemplate
11: CAny<float,32> obj2;    // Spezialisiertes Klassentemplate

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.

1: // Klassentemplate für unterschiedliche row, col Werte
2: template <typename T, int row, int col>
3: class CAny
4: {...};
5: // Partiell spezialisiertes Klassentemplate
6: // für den Fall, dass row gleich col ist
7: tempalte <typename T, int SIZE>
8: class CAny<T,SIZE,SIZE>
9: {...};
10: // Objekte Definitionen
11: CAny<short,3,5> obj1;    // Allg. Klassentemplate
12: CAny<float,4,4> obj2;    // Spezialisiertes Klassentemplate

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, kann zum Beispiel ein optimierter Algorithmus zum Zeichnen der Grafik verwendet werden. Beachten Sie, dass die Template-Definition des spezialisierten Klassentemplates nun leer ist.

1: // Allgemeines Klassentemplate
2: template <int width, int height>
3: class Image
4: {...};
5: // Partiell spez. Klassen-Tpl für den Fall width=height=32
6: template <>
7: class Image<32,32>
8: {...};
9: // Objekte Definitionen
10: Image<640,480> obj1;    // Allgemeines Klassentemplate
11: Image<32,32> obj2;      // Spezialisiertes Klassentemplate

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 entweder mittels des new Operators oder mithilfe der malloc()-Funktion reserviert. Ist der Templateparameter newAlloc true, erfolgt die Speicherreservierung mittels new und ansonsten mittels malloc().

1: // Klassentemplate
2: template <typename T, bool newAlloc>
3: class Store
4: {
5:    T* data;
6:    Store(...)
7:    {
8:       if (newAlloc)
9:          data = new T;
10:      else
11:         data = static_cast<T*>(malloc(sizeof T));
12:      ...
13:   }
14:   ...
15: };
16: // Objekt Definitionen
17: Store<int,true> intNew;
18: 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.

1: // Klassentemplates zum Reservieren von Speicher
2: template <typename T>
3: struct NewAlloc // Reserviert mit new
4: {...};
5: template <typename T>
6: struct MallocAlloc // Reserviert mit malloc()
7: {...};
8: // Klassentemplate für Daten
9: template <typename T, ???>
10: class Store
11: {...};
12: // Objekt Definitionen
13: Store<int,???> intNew;
14: Store<float,???> floatMalloc;

Bleibt damit die Frage offen, wie das neue 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.

template <typename T,
          template <typename> class TAlloc>
...

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 beide Klassentemplates für die Speicherreservierung eine Methode Create() enthalten um den Speicher zu reservieren, kann diese Methode wie angegeben aufgerufen werden. Der Platzhalter T gibt hier den Datentyp an, für den Speicher zu reservieren ist.

1: // Klassentemplates für Speicherreservierung
2: // Sowohl NewAlloc als auch MallocAlloc enthalten die
3: // Methode Create() (hier nicht dargestellt)
4: template <typename T> struct NewAlloc;
5: template <typename T> struct MallocAlloc;
6:
7: // Klassentemplate für Daten
8: template <typename T, template <typename> class TAlloc>
9: class Store
10: {
11:    T* data;
12:    Store(...)
13:    {
14:       // Speicher reservieren!
15:       data = TAlloc<T>::Create();
16:    }
17:    ...
18: };

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.

1: // Klassentemplates für Speicherreservierung
2: template <typename T> struct NewAlloc;
3: template <typename T> struct MallocAlloc;
4:
5: // Klassentemplate für Daten
6: template <typename T,
7:           template <typename> class TAlloc=NewAlloc>
8: class Store9: {...};

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.

1: // Objekt Definitionen
2: Store<int, NewAlloc> newInt;
3: Store<Demo, MallocAlloc> mallocObj;
4: 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 des TTP den gleichen Datentyp verwendet wie das aufrufende Klassentemplate (d.h., NewAlloc bzw. MallocAlloc verwenden den gleichen formalen Datentyp wie Store), muss dies nicht zwangsläufig so sein. Im Beispiel unten verwendet das Klassentemplate CAny den formalen Datentyp T1 und das Klassentemplate TP den formalen Datentyp T2. Wird Objekt vom Typ CAny definiert, sind beide Datentypen und das Klassentemplate des TTPs zu spezifizieren.

1: // Klassentemplate mit TTP
2: template <typename T1, typename T2,
3: template <typename> class TP>
4: class CAny
5: {
6:    T1 data;
7:    DoSome(...)
8:    {
9:       TP<T2>::AnyMethode();
10:   }
11:   ...
12: };
13: // Objekt Definition
14: CAny<short, float, TTPClass> obj;

Vielleicht fragen Sie sich, wofür mehrere Algorithmen zur Speicherreservierung nützlich sein sollen. In der Praxis kann es durchaus vorkommen, dass Objekte in einem bestimmten Speicherbereich abgelegt werden müssen. Denken Sie zum Beispiel einmal an eine Kommunikation zwischen zwei Prozessoren, die über ein sogenanntes shared-memory erfolgt. Ein solches shared-memory beginnt in der Regel ab einer bestimmten Adresse und ist von mehreren Prozessoren aus erreichbar. Sollen jetzt die Eigenschaften eines Objekts von beiden Prozessoren verwendet werden, kann nicht einfach der new Operator für die Definition des Objekts verwendet werden, da new Speicher auf dem Heap reserviert und nicht, wie für diesen Fall erforderlich, im shared-memory. Also schreiben Sie ein 'kleines' Klassentemplate, um den Speicher in diesem shared-memory zu verwalten und Objekte darin ablegen zu können. Wenn Sie nun in der Anwendung Objekte definieren, können Sie durch Einsatz von TTPs bestimmen, in welchem Speicherbereich das Objekt angelegt wird.

Templates als Compilezeit Ausdruck

Mit der Einführung von Templates ergab sich eine am Anfang nicht beabsichtigte Möglichkeit, bereits zur Compilezeit Anweisungen ausführen zu lassen. Dadurch wirkt der Compiler quasi wie ein Interpreter, der das Ergebnis seiner 'Berechnung' direkt in den ausführbaren Code einbaut.

Sehen wir uns dies am klassischen, aber nicht sehr praxisnahen, Beispiel der Fakultätsberechnung an.

// Beispiel für Compile-Time Ausdrücke
// Max. berechenbar ist 12! da ansonsten der Werteberich
// des unsigned long überschritten wird
#include <iostream>
// Allgemeines Klassentemplate für die Fakultätsberechnung
// Innerhalb des Klassen-Tpl wird ein neues Klassen-Tpl
// instanziiert das als Tpl-Argument den bisherigen Wert-1
// erhält
template <int val>
struct Fakultaet
{
   static const unsigned long value = val*Fakultaet<val-1>::value;
};
// Spezialisiertes Klassentemplate für Fakultät(0)
// Dies ist der Abbruch der Schleife
// der Tpl-Instanziierungen
template <>
struct Fakultaet<0>
{
   static const unsigned long value = 1;
};
int main()
{
   std::cout << Fakultaet<5>::value << '\n';
}

Zunächst wird in main() die Eigenschaft value des Templates Fakultaet<5> ausgegeben. Damit erzeugt der Compiler eine Instanz dieses Templates:

struct Fakultaet
{
   static const unsigned long value = 5*Fakulaet<4>::value;
}

Die Literale 5 und 4 sind aus dem Wert des non-type Template-Parameter val abgeleitet. Für die Berechnung von value benötigt er aber eine weitere Instanz dieses Templates, diesmal aber vom 'Typ' Fakultaet<4>:

struct Fakultät
{
   static const unsigned long value = 5*4*Fakulaet<3>::value;
}

Und dieses Spiel wiederholt sich so lange, bis der Wert des Template-Arguments schließlich 0 ist. Für diesen Wert ist das Template spezialisiert und liefert in value den Wert 1. Gleichzeitig wird damit die 'Schleife' der Template-Instanziierungen beendet und das endgültige Ergebnis in die cout-Anweisung in main() eingesetzt werden. Beachten Sie, dass diese Berechnung zur Compilezeit vorgenommen wird, da sie nur aus der Multiplikation von Konstanten besteht! Wenn Sie sich mit einem Debugger die cout-Anweisung einmal ansehen werden Sie feststellen, dass dort tatsächlich der Wert 120 (=5!) eingesetzt wird.

Dies ist zugegeben ein recht einfaches, aber nicht praxisnahes, Beispiel. Das nachfolgende Beispiel ist da schon etwas näher an der Praxis.

Da sich der Datentyp char (nicht signed char oder unsigned char!) in der Regel über Compiler-Optionen auf signed oder unsigned einstellen lässt, kann es unter Umständen zu Fehlern kommen, wenn das gleiche Programm mit unterschiedlichen Compiler-Optionen übersetzt. Das Klassentemplate WCFormat hilft, einen solchen Fehler schon zum Zeitpunkt des Übersetzens des Programms aufzudecken.

// Beispiel für Typueberpruefung zur Compile-Time

// VS2022: char = signed (default)
//                unsigned (Compile-Option /J)
// MinGW : char = signed (default)
//                unsigned (Compile-Option -funsigned-char)
#include <iostream>
#include <limits>
// Vorwärtsdeklaration eines Templates das als
// Template-Argument einen bool-Datentyp besitzt
template <bool> struct WCFormat;
// Vollständiges Template für den Fall dass der bool-
// Template-Parameter true ist.
template<> struct WCFormat<true>{};
// Funktion zur Typüberprüfung des char-Datentyps auf
// signed char.
// Dazu wird ein Objekt des Klassen-Tpl WCFormat
// instanziiert, dessen Template-Parameter nur dann true
// ist, wenn char einem signed char entspricht. Im anderen
// Fall wird versucht ein Objekt des Klassentemplates
// WCFormat zu erstellen, das aber aufgrund der
// unvollständigen Template-Definition fehlschlägt und
// damit zur Compile-Time einen Fehler erzeugt
inline void CheckCharFormat()
{
   WCFormat<std::numeric_limits<char>::is_signed == true>();
}
int main()
{
    // char-Format auf signed char testen
    CheckCharFormat();
}

Durch Abwandeln des Template-Arguments in der Funktion CheckCharFormat() lassen sich mit diesem Template auch andere Datentypen zur Compilzeit abprüfen.

Das Thema 'Templates als Compilezeit Ausdruck' ist sehr ergiebig. So lassen sich mit solchen Template-Konstruktionen u.a. Schleifen und Verzweigungen programmieren. Wenn Sie mehr darüber wissen wollen, können Sie im Internet einmal nach den Stichworten 'template metaprograms' suchen.


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