C++ Tutorial

Klassentemplates

Beginnen wir den Einstieg in die Klassentemplates wieder mit einem Beispiel. Nachfolgend sind zwei Klassen ShortStack und LongStack definiert. Der einzige Unterschied zwischen den beiden Klassen liegt nur im Datentyp des Zeigers auf die Stackdaten. Im ersten Fall ist dies ein short-Zeiger und im zweiten Fall ein long-Zeiger.

1: // Stack für short-Werte
2: class ShortStack
3: {
4:    short *pData;
5:    unsigned int index;
6: public:
7:    ShortStack(int size)
8:    {
9:       pData = new short[size];
10:      index = 0;
11:   }
12:   ~ShortStack()
13:   {
14:      delete [] pData;
15:   }
16: };
17: // Stack für auf long-Werte
18: class LongStack
19: {
20:    long *pData;
21:    unsigned int index;
22: public:
23:    LongStack(int size)
24:    {
25:       pData = new long[size];
26:      index = 0;
27:    }
28:    ~LongStack()
29:    {
30:       delete [] pData;
31:    }
32: };

Da auch die Methoden zum Ablegen der Daten auf dem Stack und zum Auslesen der Daten in ihrem Ablauf identisch sein werden, bietet es sich an, hierfür ein Klassentemplate zu entwickeln.

Definition eines Klassentemplates

Die Entwicklung eines Klassentemplates erfolgt analog zur Entwicklung eines Funktionstemplates.

Schritt 1:

Im ersten Schritt werden alle Klassendefinitionen bis auf eine entfernt und der Datentyp, der von Klasse zu Klasse unterschiedlich ist, durch einen beliebigen Namen, dem formalen Datentyp, ersetzt. Im Beispiel wurde der Datentyp wieder durch T ersetzt.

1: // Ersetzen der verschiedenen Datentypen
2: // Allgemeine Stackklasse
3: class Stack
4: {
5:    T *pData;
6:    unsigned int index;
7: public:
8:    Stack(int size)
9:    {
10:      pData = new T[size];
11:      index = 0;
12:   }
13:   ~Stack()
14:   {
15:      delete [] pData;
16:   }
17:   ...
18: };

Schritt 2:

Im zweiten Schritt müssen wir dem Compiler etwas helfen. Damit er weiß, dass T ein formaler Datentyp ist, wird vor die Klassendefinition wiederum die Anweisung

template <typename T>

gestellt.

1: // Spezifikation des formalen Datentyps
2: template <typename T>
3: class Stack
4: {
5:    T *pData;
6:    unsigned int index;
7: public:
8:    Stack(int size)
9:    {
10:      pData = new T[size];
11:     index = 0;
12:   }
13:   ...
14: };

Und damit ist die Definition des Klassentemplates im Prinzip vollständig.

Definition der Methoden

Der formale Datentyp kann ebenfalls als Parameter oder Returntyp von Methoden auftreten. So erhalten die Methoden Push() und Pop() als Parameter eine Referenz auf das abzulegende bzw. auszulesende Datum.

1: template <typename T>
2: class Stack
3: {
4:    T *pData;
5:    ...
6: public:
7:    ...
8:    bool Push(const T& val);
9:    bool Pop(T& val);
10: };

Definition der Methoden innerhalb der Klasse

Werden Methoden innerhalb der Klasse definiert, erfolgt die Definition der Methode wie gewohnt.

1: template <typename T>
2: class Stack
3: {
4:    T *pData;
5:    ...
6: public:
7:    ...
8:    bool Push(const T& val)
9:    {
10:      ...
11:      pData[sIndex++] = val;
12:   }
13:   bool Pop(T& val)
14:   {
15:      ...
16:      val = pData[--sIndex];
17:   }
18: };

Definition der Methoden außerhalb der Klasse

Werden Methoden außerhalb des Klassentemplates definiert, ist eine auf den ersten Blick etwas verwirrende Definition erforderlich.

template <typename T>
RTYP CLASS<T>::MName(PARAM)
{...}

T ist wieder der formale Datentyp, RTYP der Returntyp der Methode, CLASS der Name des Klassentemplates und MName der Name der Methode. Das nachfolgende Beispiel zeigt die Definitionen der Methoden Push() und Pop() der Klasse Stack.

1: template <typename T>
2: class Stack
3: {
4:    T *pData;
5:    ...
6: public:
7:    ...
8:    bool Push(const T& val);
9:    bool Pop(T& val);
10: };
11:
12: template <typename T>
13: bool Stack<T>::Push(const T& val)
14: {...}
15:
16: template <typename T>
17: bool Stack<T>::Pop(T& val)
18: {...}

Definition von Objekten

Da bei der Definition des Klassentemplates nur der formale Datentyp angegeben wurde, ist bei der Definition eines Objekts der hierfür einzusetzende Datentyp nach dem Klassennamen in spitzen Klammern anzugeben. Im Beispiel wird zunächst ein Stack-Objekt für die Aufnahme von 10 long-Werten definiert und anschließend ein weiteres Stack-Objekt für die Aufnahme von 50 float-Werte.

1: // Definition eines Objekts eines Klassentemplates:
2: // Template-Definition
3: template <typename T>
4: class Stack
5: {
6:    T *pData;
7: public:
8:    Stack(int size);
9:    ...
10: };
11: // Objektdefinition + Template-Instanziierung
12: Stack<long> longStack(10);
13: Stack<float> floatStack(50);

Und erst bei der Definition eines Objekts werden durch den Compiler die Member des Klassentemplates instanziiert.

Überladen von Methoden

Wie bei Funktionstemplates können die Methoden eines Klassentemplates überladen werden.

Angenommen es gibt ein Klassentemplate, das ein beliebiges Datum abspeichert. Zu diesem Datum soll nun mittels zweier Methoden Add() ein weiteres Datum addiert werden. Die erste Methode Add() erhält als Parameter eine Referenz auf das zu addierende Datum und die zweite einen Zeiger darauf.

#include <print>
// Template-Definition
template <typename T>
class Tpl
{
   T data;
public:
   // ctor
   Tpl(T nV): data(nV)
   {}
   T Get() const
   {
      return data;
   }
   // Addierte Datum per Referenz
   void Add(const T& nV)
   {
      data += nV;
   }
   // Addiere Datum per Zeiger
   void Add(T* ptr)
   {
      data += *ptr;
   }
};
int main()
{
   Tpl<int> obj(0);     // Tpl-Objekt
   int var = 10;        // beliebige int-Variable
   obj.Add(var);        // void Add(T& nV)
   obj.Add(&var);       // void Add(T* ptr)
   std::println("Summe: {}",obj.Get());
}

Mehrere formale Datentypen

Wie Funktionstemplates können auch Klassentemplates mehrere formale Datentypen besitzen. Die formalen Datentypen werden bei der Definition des Klassentemplates in der template-Anweisung aufgelistet und bei der Definition eines Objekts sind die entsprechenden Datentypen anzugeben.

1: // Template-Definition
2: template <typename T1, typename T2>
3: class MyClass
4: {...};
5: // Objektdefinitionen
6: MyClass<int, char*> myObj1;
7: MyClass<float, double> myObj2;

Non-type Parameter

Ebenso können Klassentemplates non-type Parameter besitzen. Ein non-type Parameter kann sein:

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

Der non-type Parameter wird, wie die formalen Datentypen, innerhalb der template-Anweisung aufgeführt und besteht aus dem Datentyp, dem Parameternamen und einem optionalen Defaultwert. Bei der Instanziierung eines Objekts wird dann der non-type Parameter durch das übergebene Argument ersetzt. Dadurch ist es z.B. möglich, als Eigenschaft Felder mit unterschiedlichen Feldgrößen bei der Definition von Objekten zu definieren. Für das Template Stack könnte dies wie folgt aussehen:

1: template <typename T, int SIZE=5>
2: class Stack
3: {
4:    T sdata[SIZE];   // Feldgroesse ist SIZE
5:    ...
6: };

Wird die Template-Methode außerhalb des Klassentemplates definiert, ist der non-type Parameter dort ebenfalls anzugeben, allerdings ohne einen eventuellen Defaultwert.

1: template <typename T, int SIZE>
2: bool Stack<T, SIZE>::Pop(T& data)
3: {
4:    ...
5: }

Gleichfalls ist die Definition eines Objektes des Klassentemplates anzupassen, da hier der non-type Parameter mit anzugeben ist (wenn kein Defaultwert definiert ist).

Stack<int,10> intStack;
Stack<float> floatStack

Im ersten Fall wird ein Stack für 10 int-Werte angelegt und im zweiten Falle einer für 5 float-Werte.

Default-Datentyp

Genauso wie bei Methoden/Funktionen Defaultwerte für Parameter vorgegeben werden können, können für die formalen Datentypen Default-Datentypen vorgegeben werden. Dazu werden nach dem formalen Datentyp der Zuweisungsoperator und anschließend der Default-Datentyp angegeben. Wird bei der Definition eines Objekts des Klassentemplates kein Datentyp explizit angegeben, wird der Default-Datentyp verwendet. Im Beispiel wird zuerst ein Stack für int-Daten erzeugt und als Nächstes ein Stack für float-Daten.

1: // Definition des Klassentemplates
2: template <typename T=int>
3: class Stack
4: {
5:    ...
6: };
7: // Definition von Objekten
8: Stack<>      intStack;
9: Stack<float> floatStack;

using Anweisung und Templates

Mithilfe der using-Anweisung ist es möglich, Synonyme für Templates zu definieren.

1: // Definition Klassentemplate
2: template <typename T, int SIZE>
3: class SArray
4: {
5: ...
6: };
7:
8: // Synonyme definieren
9: template <int S>
10: using shortArray = SArray<short,S>;  // SArray short Werte
11: template <int S>
12: using longArray = SArray<long,S>;    // SArray long Werte
13:
14: // Objektdefintion
15: shortArray<10> array1;    // Array 10 short Werte
16: longArray<50> array2;     // Array 50 long Werte

Extern und Klassentemplates

Wie bei Funktionstemplates instanziiert der Compiler defaultmäßig erst dann ein Klassentemplate, wenn ein Objekt definiert wird. Dieses Verhalten führt aber dazu, dass auf Basis von Templates keine Bibliothek aufgebaut werden könnte, da das Objekt erst in der Anwendung definiert wird und nicht in der Bibliothek.

Die Lösung für dieses Problem lautet: externe Template-Instanziierung. Hierbei wird das Klassentemplate in der Bibliothek instanziiert und in der Anwendung dann mittels extern auf diese Instanz verwiesen. D.h. für Klassentemplates kann das gleiche Verfahren angewandt werden wie für externe Variablen. Sehen wir uns dies an einem Beispiel an.

Datei bibl.h mit der Template-Definition:

1: template <typename T>
2: class CAny
3: {
4:    T data;
5: public:
6:    CAny (T val): data(val)
7:    {}
8:    T GetData()
9:    {
10:      return data;
11:   }
12: };

Datei bibl.cpp mit den Template-Instanzen:

1: #include <string>
2: #include "bibl.h"
3:
4: template class CAny<int>;           // Tpl mit int
5: template class CAny<std::string>;   // Tpl mit string

Die Applikation:

1: #include "bibl.h"
2: using namespace std::string_literals;
3:
4: extern template class CAny<int>; // Externe Instanzen
5: extern template class CAny<std::string>; // des Klassen-Tpls
6:
7: int main()
8: {
9:    // Die folgenden Anweisung legen keine neue
10:   // Instanz des Klassentemplates mehr an
11:   CAny<int> ival(11);
12:   CAny<std::string> sval("hurra!"s);
13:   ...
14: }

In der Datei bibl.h wird das Klassentemplate zunächst wie gewohnt deklariert. Anschließend werden in der Datei bibl.cpp zwei Instanzen des Klassentemplates erzeugt, einmal für int-Daten und einmal für string-Daten. Diese Instanziierungen bewirken, dass alle Methoden des Klassentemplates für int- und string-Daten durch den Compiler erstellt werden, aber kein Speicher für die Eigenschaften reserviert wird. In der Anwendung wird dann mittels extern auf diese Instanzen verwiesen, was dazu führt, dass nun der Speicher für Eigenschaften reserviert wird. Ohne die Angabe von extern würde der Compiler erneut in der Anwendung den Code für die verwendeten Methoden des Klassentemplates erzeugen und erst beim Linken der Dateien würden dann diese "überzähligen" Methoden wieder entfernt.


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