Templateklassen

Einführung

In C++ gibt es die sogenannte Vorlagenklassen. Sie dienen dazu bestimmte Klassen zunächst allgemein zu deklarieren. Erst bei der Instanzierung werden, die zuvor allgemein gehaltenen "Elemente" definiert. Am gebräuchlichsten ist dabei die Variante, Datentypen die eine Klasse für eigene Member verwendet bei der Deklaration zunächst noch nicht festzulegen. Erst wenn dann eine Instanz dieser Klasse gebildet wird, wird festgelegt welcher Datentyp für die Member zu verwenden ist. Es ist jedoch auch möglich Werte beliebigen Datentyps bei der Bildung einer Instanz mit anzugeben, die aus irgendwelchen Gründen nicht über den Konstruktor angegeben werden sollen. Für jede Instanz, die dann innerhalb eines Programms gebildet wird und sich in Bezug auf die übergebenen Werte von anderen Instanzen unterscheidet, wird eine neue Klasse generiert. Dies hat zur Folge, dass die zunächst allgemein gehaltenen Elemente einer Vorlagenklasse nur durch "Elemente" ersetzt werden können, die zur Zeit der Kompilierung bereits definiert sind. Aufgrund dessen ist es es also unmöglich Werte, die erst zur Laufzeit bestimmt werden, für Vorlagenklassen zu verwenden. Außerdem ist es aus dem selben Grund auch nicht möglich eine Vorlagenklasse in einer Bibliothek bereitzustellen, um von anderen Bibliotheken oder Programmen importiert zu werden. Deshalb besteht eigentlich auch kein Grund die Deklaration und die Implementierung einer Vorlagenklasse auf zwei Dateien aufzuteilen. Besteht ein Projekt aus mehreren Dateien, dann sollte eine Vorlagenklasse möglichst in einer Headerdatei deklariert und implementiert werden, die dann von allen anderen Dateien, in der die Klasse benötigt wird, inkludiert werden kann.

Deklaration

Wird einer Klassendeklaration eine Liste von Vorlagen, die durch das Schlüsselwort 'template' eingeleitet wird, vorangestellt, dann gilt diese Klasse als Vorlagenklasse. Die Bezeichner, die für die einzelnen Vorlagen gewählt werden (bei der Namensgebung gelten dieselben Bestimmung, wie bei Variablen), sind dann innerhalb der Klasse bekannt und können nach belieben verwendet werden. Die allgemeine Deklaration sieht dann wie folgt aus:

template < <vorlage1>, <vorlage2>, ... , <vorlagen> >
class <vorlagenklass>
{
	/* --- Deklaration der Member ---
	...
	*/
};

Wie bereits erwähnt, können Vorlagen entweder Werte oder auch Datentypen darstellen. Um eine Vorlage als Datentyp zu definieren, kann man entweder das Schlüsselwort 'typename' oder aber einfach das Schlüsselwort 'class' verwenden. Im folgenden wird eine Vorlagenklasse definiert, die zwei Datentypen ('T1' und 'T2') und eine Ganzzahl ('Length') als Vorlage verwendet. Die Datentypen sollen dabei für zwei Membervariablen ('m_var' und 'm_arr') verwendet werden. Dabei soll die zweite Variable ('m_arr') ein statischer Array der Länge 'Length' sein:

template <typename T1, class T2, unsigned int Length>
class ATemplate
{
private:
	T1 m_var;
	T2 m_arr[Length];
public:

};

Nun sollen noch zwei Funktion implemetiert werden. Die erste Funktion 'Data' soll den Wert von 'm_var' ändern können, während die zweite 'DataAt' den Zugriff auf die Elemente des Arrays ermöglicht. Dabei besteht wiederum die Möglichkeit die Implementierung innerhalb oder aber außerhalb der Klasse durchzuführen. Soll dei Funktion außerhalb der Klasse implementiert werden, dann reicht es allerdings nicht aus nur den Klassennamen 'ATemplate' für den Operator '::' zu verwenden. Es muss für jede Funktion, die außerhalb einer Vorlagenklasse implementiert wird, erneut die Liste der Vorlagen mit angegeben werden. Außerdem müssen dann diese Vorlagen noch einmal zusammen mit dem Klassennamen angegeben werden. Um beide Möglichkeiten aufzuzeigen, wird die Funktion 'Data' innerhalb und die Funktion  'DataAt' außerhalb der Klasse implementiert:

template <typename T1, class T2, unsigned int Length>
class ATemplate
{
private:
	T1 m_var;
	T2 m_arr[Length];
public:
	T1& Data(void) {return m_var;}
	T2& DataAt(unsigned int);
};

//Implementierung von DataAt
template <typename T1, class T2, unsigned int Length>
T2& ATemplate<T1, T2, Length>::DataAt(unsigned int Index)
{
	if (Index >= Length) Index = Length - 1;
	return m_arr[Index];
}

Nun ist die Klasse noch ziemlich unsicher. Wenn nämlich die Arraylänge mit '0' angegeben wird und man die Funktion 'DataAt' aufruft, dann kann keine gültige Referenz zurückgegeben werden. So könnte man als weitere Vorlage einen Wert übergeben, der einen ungültigen Wert symbolisiert, den Array um ein Element vergrößern und das letzte Element im Konstruktor mit dem ungültigen Wert initialisieren:

template <typename T1, class T2, unsigned int Length, T2 Invalid>
class ATemplate
{
private:
	T1 m_var;
	T2 m_arr[Length + 1];
public:
	ATemplate(void) {m_arr[Length] = Invalid;}
	T1& Data(void) {return m_var;}
	T2& DataAt(unsigned int);
};

//Implementierung von DataAt
template <typename T1, class T2, unsigned int Length, T2 Invalid>
T2& ATemplate<T1, T2, Length, Invalid>::DataAt(unsigned int Index)
{
	if (Index > Length) Index = Length;
	return m_arr[Index];
}

Wird ein Vorlagenparameter hinzugefügt, dann muss darauf geachtet werden, dass diese Änderung überall, also in diesem Falle auch bei der Implementierung der Funktion 'DataAt' durchgeführt wird. Interessant bei dieser Vorlage ist, die Tatsache, dass ein Datentyp als Vorlage verwendet wird und eine weitere Vorlage benutzt wird, welche eben diesen "Vorlagendatentyp" verwendet. 

Instanzen von Vorlagenklassen

Um nun eine Instanz einer Vorlagenklasse zu bilden, müssen neben dem Klassennamen und dem Variablennamen auch die gewünschten Parameter für die Vorlagen eingegeben werden. Soll also beispielsweise die Klasse 'ATemplate' verwendet werden, um für den statischen Array ('m_arr') den Datentyp 'int' und die Länge '5', für die Variable 'm_var' den Datentyp 'double' und als ungültigen Wert '-1' zu benutzen, dann die Bildung einer Instanz 't1' wie folgt aus:

ATemplate<double, int, 5, -1> t1;

Nun lassen sich die Memberfunktionen 'DataAt' und 'Data' wie gewohnt aufrufen:

//m_var initialisieren
t1.Data() = 3.14;

//m_arr initialisieren
int* pEl = &t1.DataAt(0);
while (*pEl  != -1)
{
	*pEl = 0;
	pEl++; 
}

Nun wird der gesamte Array mit '0' und die Variable 'm_var' mit '3.14' initialisiert. Natürlich lässt sich eine Vorlagenklasse wie 'ATemplate' auch mit anderen Datentypen verwenden. So wäre es beispielsweise auch möglich eine Klasse wie etwa 'CRect' bei der Angabe des Datentyps anzugeben. Allerdings müsste für den Parameter 'Invalid'  zunächst einmal eine Instanz eben dieser Klasse gebildet werden. Diese kann jedoch nicht übergeben erden, da eine Variable übergeben wird und kein Wert. Die Übergabe einer Variablen bei einer Vorlagenklasse ist jedoch nicht möglich. Um dennoch eine Vorlagenklasse vorweisen zu können, bei der sich auch Klassen angeben lassen, folgt hier eine weitere Vorlagenklassendeklaration:

template <typename T>
class CSimpleTemplateArray
{
private:
	T *m_pData;
	int m_Length;
	
	//Kopiermethode
	void _Copy(const CSimpleTemplateArray<T>& arr)
	{
		//Zunächst prüfen ob die Instanz sich selbst kopieren soll
		if (&arr == this) return;

		//erst evtl. bestehende Daten löschen
		if (m_pData) delete[] m_pData;

		//Arraylänge übernehmen
		m_Length = arr.m_Length;
	
		//neuen Speicher reservieren
		m_pData = new T[m_Length];
	
		//Element für Element kopieren:
		for (int iPos = 0; iPos < m_Length; iPos++)
		{
			m_pData[iPos] = arr.m_pData[iPos];
		}
	}
public:
	//Konstruktor
	CSimpleTemplateArray(int Length = 0)
	{
		if (Length <= 0) m_Length = 1;
		else m_Length = Length;
		
		//Speicher reservieren
		m_pData = new T[m_Length];
	}
	
	//Kopierkonstruktor
	CSimpleTemplateArray(const CSimpleTemplateArray<T>& arr)
	{
		//Daten initialisieren
		m_pData = 0;
		m_Length = 0;

		_Copy(arr);
	}

	int GetLength(void)
	{
		return m_Length;
	}
	
	//Destruktor
	~CSimpleTemplateArray(void)
	{
		if (m_pData) 
		{
			delete[] m_pData;
			m_pData = 0;
	
		}
	}
	
	//Wert setzen
	T& DataAt(int Index)
	{
		if (Index < 0 || Index >= m_Length) 
		{Index = 0;}
		
		return *(m_pData + Index); 
	}

	//Operator '='
	CSimpleTemplateArray<T>& operator=(const CSimpleTemplateArray<T>& arr)
	{
		_Copy(arr);
		
		//Instanz zurückgeben
		return *this;
	}

	//Operator '[]'
	T& operator[](int Index)
	{
		return DataAt(Index);
	}
};

Die Klasse sollte einem bekannt vorkommen. Diese Klasse entspricht nämlich der Klasse 'CSimpleArray'. Nur dieses Mal ist der Datentyp, der einzelnen Elemente nicht festgelegt. Er lässt sich über eine Vorlage festlegen. Der einzige Unterschied ist im Prinzip (neben der Einführung des allgemeinen Typs 'T') die Art und weise, wie zwei Arrays kopiert werden. Zuvor wurde die Methode 'memcpy' benutzt. Nun werden die einzelnen Elemente Element für Element durch den Zuweisungsoperator kopiert. Auf diese Weise wird nämlich in den Fällen, in denen der zugrundeliegende Typ eine Kasse repräsentiert, sichergestellt, dass die Daten so kopiert werden, wie es von der Klasse gewünscht wird. Ansonsten würden alle Elemente bitweise kopiert. (Die möglichen Konsequenzen sind ja bekannt.) Nun lässt sich also auch ein Array von 'CRect'-Inszanzen erstellen. Im folgenden Beispiel wird ein solcher Array erstellt wobei anschließend jedes Element initialisiert wird. Außerdem wird von jedem Element die Fläche berechnet und diese in einem anderen Array gespeichert:

CSimpleTemplateArray<int> arrArea(5);
CSimpleTemplateArray<CRect> arrRects(5);

for (int iPos = 0; iPos < arrRects.GetLength(); iPos++)
{
	CRect *pr = &arrRects.DataAt(iPos);
	pr->SetWidth(iPos * 100);
	pr->SetHeight(iPos * 100);

	arrArea.DataAt(iPos) = pr->GetArea();
}

Statische Member in Vorlagenklassen

Natürlich lassen sich auch in Vorlagenklassen statische Member verwenden. Allerdings muss man sich dann darüber im klaren sein, dass eine Vorlagenklasse eben nur als Vorlage für eine Klasse darstellt. Wie bereits erwähnt, wird nämlich immer wenn eine Vorlagenklasse neu verwendet wird, dann wird vom Compiler eine ganz neue Klasse generiert. Somit besitzt, dann jede dieser Klasse eine eigene Kopie dieser statischen Variablen. Betrachtet man dazu noch einmal die Klasse 'ATemplate' und ergänzt diese Klasse um eine statische Variable 's_var', die öffentlich ist und somit nach belieben verändert werden kann, dann könnte die Klasse nun wie folgt aussehen:

template <typename T1, class T2, unsigned int Length, T2 Invalid>
class ATemplate
{
private:
	T1 m_var;
	T2 m_arr[Length + 1];
public:	
	static int s_var;

	ATemplate(void) {m_arr[Length] = Invalid;}
	T1& Data(void) {return m_var;}
	T2& DataAt(unsigned int);
};

template <typename T1, class T2, unsigned int Length, T2 Invalid>
int ATemplate<T1, T2, Length, Invalid>::s_var = 0;

//Implementierung von DataAt
template <typename T1, class T2, unsigned int Length, T2 Invalid>
T2& ATemplate<T1, T2, Length, Invalid>::DataAt(unsigned int Index)
{
	if (Index > Length) Index = Length;
	return m_arr[Index];
}

An folgendem Aufruf erkennt man schnell, dass die statische Membervariable nicht für alle von der Vorlagenklasse erstellten Klassen dieselbe ist, sondern lediglich innerhalb jeder neu erstellten Klasse statisch ist:

ATemplate<int, int, 5, -1> t1;
t1.s_var = 3;

ATemplate<int, int, 1, -1> t2;
cout << "s_var: " << t2.s_var << endl;			//Ausgabe: "s_var: 0"
t2.s_var = 5;

cout << "s_var: " << 
	ATemplate<int, int, 5, -1>::s_var << endl;	//Ausgabe: "s_var: 3"

Um tatsächlich eine statische Membervariable für alle Klassen, die auf Grundlage der Vorlagenklasse entstanden ist, kann man einfach von einer Klasse ableiten (dies geht selbstverständlich bei Vorlagenklassen), die eben diese statische Membervariable enthält. Verlagert man also die statische Membervariable 's_var' in eine Klasse namens 'StaticBase' und lässt anschließend 'ATemplate' von dieser Klasse ableiten, dann existiert tatsächlich nur ein 's_var' für alle Klassen, die aus der Vorlagenklasse entstanden sind. Dazu kurz noch einmal die durchgeführten Änderungen:

class StaticBase
{
public:
	static int s_var;
};

int StaticBase::s_var = 0;

template <typename T1, class T2, unsigned int Length, T2 Invalid>
class ATemplate : public StaticBase
{
private:
	T1 m_var;
	T2 m_arr[Length + 1];
public:	
	ATemplate(void) {m_arr[Length] = Invalid;}
	T1& Data(void) {return m_var;}
	T2& DataAt(unsigned int);
};

//Implementierung von DataAt
template <typename T1, class T2, unsigned int Length, T2 Invalid>
T2& ATemplate<T1, T2, Length, Invalid>::DataAt(unsigned int Index)
{
	if (Index > Length) Index = Length;
	return m_arr[Index];
}

Nun liefert der obige Quelltext tatsächlich die Ergebnisse, die man vielleicht eher erwartet hätte:

ATemplate<int, int, 5, -1> t1;
t1.s_var = 3;

ATemplate<int, int, 1, -1> t2;
cout << "s_var: " << t2.s_var << endl;			//Ausgabe: "s_var: 3"
t2.s_var = 5;

cout << "s_var: " << 
	ATemplate<int, int, 5, -1>::s_var << endl;	//Ausgabe: "s_var: 5"

Standardparameter bei Vorlagenklassen

Auch bei Vorlagen lassen sich Standardparameter verwenden. Dies geschieht genauso wie bei Standardparametern bei Funktionen. So ließe sich z.B. standardmäßig festlegen, dass die Klasse 'CSimpleTemplateArray' standardmäßig den Datentyp 'int' verwendet:

template <typename T = int>
class CSimpleTemplateArray
{
	/* --- Deklaration der Member ---
	...
	*/
};

Bei der Bildung einer Instanz von 'CSimpleTemplateArray' kann nun auf folgende Weise einfach der Standardparameter für den zugrundeliegenden Datentyp (also 'int') verwendet werden:

CSimpleTemplateArray<> arr(5);
arr.DataAt(0) = 10;

Einschränkungen verwendbarer Datentypen

Ist ein übergebener Datentyp als Vorlagenparameter und ist der Vorlagenklasse bekannt, welche Member diese Klasse enthält, dann kann diese auch auf diese Member zugreifen. Angenommen eine Klasse implementiert die Funktion 'GetName', die einfach eine Zeichenkette mit dem Namen der Klasse zurückgibt, dann kann eine Vorlagenklasse, der die Klasse als zu verwendender Datentyp übergeben wird, auf diese Funktion zugreifen um den Namen der Klasse zu ermitteln. Dazu folgendes Beispiel:

class AClass
{
public:
	static const char* GetName(void)
	{
		return "AClass";
	}
};

Eine Vorlagenklasse könnte den Namen der enthaltenen Klasse während des Konstruktoraufrufs auf dem Bildschirm ausgeben:

template <class T>
class ATemplateClass
{
public:
	ATemplateClass(void)
	{
		cout << "Die Klasse kann Instanzen der Klasse \'" 
			<< T::GetName() << "\' bilden." << endl;
		T t;
		cout << "Von der Klasse \'" << t.GetName() 
			<< "\' wurde eine Instanz erstellt." << endl;
	}
};

Wird nun eine Instanz von 'ATemplateClass' gebildet, bei der als Vorlagenarameter für den Type 'T' die Klasse 'AClass' angegeben wird, dann erscheinen die gewünschte Ausgaben:

//Ausgabe: "Die Klasse kann Instanzen von 'AClass' bilden."
//Ausgabe: "Von der Klasse 'AClass' wurde eine Instanz erstellt."
ATemplateClass<AClass> t;

Sollte jedoch für den Datentyp 'T' eine Klasse, die die Funktion 'GetName' nicht implementiert oder gar ein Datentyp, wie etwa 'int',  angegeben werden, dann hagelt es Fehler. Auf diese Weise können verwendbare Typen für eine Vorlagenklasse eingeschränkt werden. So könnte man etwa eine Basisklasse (kann auch abstrakt sein) verwenden, die alle benötigten Funktionen deklariert, die in der Vorlagenklasse benötigt werden. Alle Klassen, die dann von dieser Klasse abgeleitet sind lassen sich dann innerhalb der Klasse verwenden. Sobald jedoch innerhalb der Vorlagenklasse für irgendeinen Vorlagentyp irgendein Operator verwendet wird, der den Zugriff auf Klassenmember ermöglicht (also: '::' , '.' oder '->'), ist diese Vorlagenklasse nicht mehr für Standarddatentypen (wie etwa 'int') zu gebrauchen. Es besteht auch die Möglichkeit eine Vorlagenklasse von einem (oder mehreren) in der Vorlagenliste definierten Datentypen abzuleiten. Auch in diesem Falle können natürlich nur Klassen als Datentypen verwendet werden.

Für die nächsten Lektionen

Nun sind also auch Vorlagenklassen bekannt. In der nächsten Lektion wird es vornehmlich um Programmiertechniken gehen, die die OOP betreffen. Dort werden auch Vorlagenklassen immer mal wieder aufgegriffen. Zu diesem Zweck sollten Deklaration und Implementierung solcher Klassen geläufig sein.

Zurück Nach oben Weiter