Twitter und Facebook-Anbindung
X
Tweet Follow @twitterapi
!!! Anbindung an twitter und facebook öffnen !!!

Wenn Ihnen mein Online-Buch gefällt,
dann bedanken Sie sich doch mit einer kleinen Spende...

24 Templates

24 Templates

In diesem Kapitel werde ich eine Möglichkeit vorstellen, mit welcher man noch mehr Flexibilität bekommt. Es handelt sich um den Mechanismus der Templates (auch Muster genannt). Mit ihnen kann man sich das Leben als Programmierer noch mehr vereinfachen als mit Klassen, allerdings möchte ich die Euphorie noch in Grenzen halten, da sich aus Templates ein paar wesentliche Nachteile ergeben.

Zum Seitenanfang
Zum Inhaltsverzeichnis

24.3 Funktionstemplates

24.3 Funktionstemplates

Als erstes fange ich mit Funktionstemplates an, weil man hier am schnellsten zeigen kann, was eigentlich im Hintergrund passiert. Im ersten Schritt werde ich also ein ganz kleines Template entwerfen, welches nichts anderes macht, als zwei Datentypen zu addieren. Das macht zwar in der Realität absolut keinen Sinn, weil man für so etwas keine Funktion braucht, aber man sieht sehr schön was passiert und wie es gemacht wird. Schauen wir uns mal an, wie so ein kleines Projekt mit einem solchen Template aussehen könnte.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
					
#include <stdio.h>

// Funktionstemplate //////////////////////////////////////////////////////////
template<typename TResultDataType, typename TParameterDataType>
TResultDataType Addiere(TParameterDataType tElement1, TParameterDataType tElement2) {
	return static_cast<TResultDataType>(tElement1) + static_cast<TResultDataType>(tElement2);
} // Addieren /////////////////////////////////////////////////////////////////



// Hauptfunktion der Anwendung ////////////////////////////////////////////////
int main(int argc, char** argv) {
	int iValue1		= 10;
	int iValue2		= 20;
	long int iErgebnis	= Addiere<long int, int>(iValue1, iValue2);

	double dValue1		= 1.5;
	double dValue2		= 2.7;
	float fErgebnis	= Addiere<float, double>(dValue1, dValue2);

	printf("Ergebnis der Addition ist %li\n", iErgebnis);
	printf("Ergebnis der Addition ist %g\n", fErgebnis);

	return 0;
} // main /////////////////////////////////////////////////////////////////////
					

Ausgabe:

Ergebnis der Addition ist 30
Ergebnis der Addition ist 4.2
		

In Zeile 4 bis 7 siehen Sie, wie man ein Funktionstemplate implementiert. Jedes Template wird mit dem Schlüsselwort "template" eingeleitet, gefolgt von einer geöffneten spitzen Klammer, danach eine Parameterliste, einer schließenden spitzen Klammer und abschließend der Definition der Funktion oder Klasse (letzteres erkläre ich später). Was ist mit Parameterliste gemeint? Sie beinhaltet meistens eines der drei Schlüsselwörter "typename", "class" oder "template", gefolgt von einem frei wählbaren Typenbezeichner, welchen man innerhalb des Templates benutzt. Anschließend kann man mit Komma getrennt, weitere dieser Paare definieren. Den Unterschied zwischen "typename" und "class", werde ich im nächsten Unterkapitel erläutern. Auf den Parametertyp "template" werde ich nicht weiter eingehen, da man es zum einen selten benutzt und es zum anderen die ganze Sache nicht leichter macht. Gemeint ist aber, dass man innerhalb eines Templates, ein existierendes Template einbinden kann. Somit erreicht man eine Verschachtlung.

Nach der geschlossenen Spitzen Klammer in Zeile 4, steht nun der eigentliche Funktionskopf in Zeile 5. Normalerweise schreibt man dies in eine Zeile, aber mit einem Zeilenumbruch, sieht es übersichtlicher aus. Zuerst steht der erste definierte Datentypbezeichner als Rückgabewert, dann der Funktionsname und abschließend, in den runden Klammern, die Übergabeparameter, jeweils vom zweiten definierten Typenbezeichner und natürlich noch die Variablennamen.

Zeile 6 ist nicht weiter spannend, da hier nichts atemberaubendes passiert. Ich caste lediglich die zwei übergebenen Parameter, in den Ergebnistyp um, bevor ich sie miteinander addiere. Bei der Addition spielt dies keine große Rolle, aber im Falle einer Division wäre dies wichtiger.

Interessanter wird es dann aber wieder in der "main", in den Zeilen 12 bis 25. Hier benutze ich nun das eben definierte Template. In den Zeilen 15 und 19 rufe ich das Funktinstemplate auf und wie Sie sehen können, muss man auch hier wieder die spitzen Klammern verwenden. Innerhalb dieser Klammern, gibt man jetzt den Ergebnisdatentyp und den Parameterdatentyp an, mit welcher das Template arbeiten soll (Sie erinnern sich vielleicht noch, dass diese Syntax auch bei den verschiedenen Castings benutzt wurde). Dies hat zur Folge, dass einmal jedes "TResultDataType" in "long int" und jedes "TParameterDataType" in "int" umgewandelt wird und das andere mal jedes "TResultDataType" durch "float" und "TParameterDataType" durch "double" ersetzt wird. Der Compiler generiert also folgenden Quelltext im Hintergrund, wobei die eigentliche Templatedefinition restlos verschwindet.

 1
 2
 3
 4
 5
 6
 7
					
long int Addiere(int tElement1, int tElement2) {
	return static_cast<long int>(tElement1) + static_cast<long int>(tElement2);
}

float Addiere(double tElement1, double tElement2) {
	return static_cast<float>(tElement1) + static_cast<float>(tElement2);
}
					

Hier sehen Sie jetzt, dass das Template wirklich als Kopiervorlage benutzt wurde und das für "TResultDataType" und "TParameterDataType" die gewünschten Typen ersetzt wurden. Ich betone noch einmal an dieser Stelle, dass diese Ersetzung nur stattfindet, wenn man das Template benutzt. Vergisst man also innerhalb des Templates z.B. ein Semikolon oder ähnliches, ohne das Template einzubinden, wird der Compiler keinen Fehler erkennen, da das Template schlicht und einfach, genauso wie Kommentare, weggelassen bzw. rausgeschmissen wird.

Wie gesagt, dieses Beispiel macht nicht viel Sinn, aber man sieht schön, dass ich aus einer händisch geschriebenen Funktion, unterm Strich zwei verschiedene generiert bekommen habe. Würde ich dieses Template noch für zehn andere Datentypen verwenden, würden entsprechend noch zehn weitere Funktionen generiert werden.

Ein denkbarer Anwendungsfall für Funktionstemplates ist z.B., dass man Elemente aus einer Binärdatei entnehmen, sortieren und anschließend wieder in eine andere Datei speichern will, denn um auf Binärdateien zugreifen zu können, benötigt man immer einen speziellen Datentyp. Falls es nun Dateien geben könnte mit Integern und welche mit Floats, kann man sich ein Template bauen, welches den Typen erst einmal rein formal behandelt und anschließend ruft man das Funktionstemplate mit dem entsprechenden Datentyp auf.

Zum Seitenanfang
Zum Inhaltsverzeichnis

24.5 Weiteres zu Templates

24.5 Weiteres zu Templates

Die ganze Thematik der Templates ist sehr umfangreich und ich werde nicht auf jede Besonderheit eingehen, aber ein paar Sachen möchte ich noch im Ansatz erwähnen, da sie den Umgang erleichtern. Sie sollten dieses Unterkapitel aber nur lesen, wenn Sie die letzten Kapitel vollständig verstanden haben, denn ich nehme mit jetzt nicht mehr die Zeit, alles bis ins kleinste Detail zu erklären. Ich werde wirklich nur noch zu den neuen Sachen ein paar Worte verlieren.

Zum Seitenanfang
Zum Inhaltsverzeichnis

24.5.1 Defaulttypen

24.5.1 Defaulttypen

Genauso wie Sie das von Funktionen her kennen, ist es möglich, den Templateparametern Defaultwerte zuzuweisen. Dies hat zur Folge, dass der Aufruf etwas vereinfacht wird. Im folgenden Beispiel werde ich dies kurz anhand einer Templatefunktion zeigen. Dieser Mechanismus funktioniert aber auch bei Templateklassen.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
					
// Gibt das Ergebnis der Division zurück //////////////////////////////////////
template<typename TResultType = float>
TResultType Dividieren(int iDivident, int iDivisor) {
	// Wenn der Divisor gleich 0 ist
	if (iDivisor == 0) {
		return static_cast<TResultType>(0);
	} else {
		return static_cast<TResultType>(iDivident) / static_cast<TResultType>(iDivisor);
	} // end of if
} // Dividieren ///////////////////////////////////////////////////////////////

// ...

printf("%i ", Dividieren<int>(10, 4));	// Nutzt expliziten Typen
printf("%g\n", Dividieren<>(10, 4));	// Nutzt Defaulttypen
					

Ausgabe:

2 2.5
		

Wie Sie sehen können, habe ich in Zeile 2 hinter den Datentyp noch einen Defaultdatentyp angegeben und somit benötige ich keine Angabe beim Aufruf, wie Sie in Zeile 15 sehen können. Wichtig ist aber, dass man trotzdem die spitzen Klammern schreibt.

Zum Seitenanfang
Zum Inhaltsverzeichnis

24.5.5 Type Traits

24.5.5 Type Traits

Wie ich mehrfach erwähnte, ist es eher unpraktisch, ein und die selbe Methode mehrfach zu definieren, nur um alle Datentypen abzudecken. Es stellt sich also die Frage, ob man dies nicht eleganter lösen kann. Hier kommen die s.g. Type Traits ins Spiel. Die Grundidee ist, dass man einen Mechanismus baut, mit welchem man später erkennt, um welchen Templatetypen es sich handelt und dann mit einer einfachen if Anweisung unterscheidet. So wird zwar die Implementierung der entsprechenden Methode größer, aber man braucht nur noch eine. Hier also mal ein vereinfachtes Beispiel für einen Type Trait.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
					
template<typename Ttype> struct STypeTrait {
	static const bool	IsPointer	= false;
	static const bool	IsInt		= false;
	static const bool	IsFloat		= false;
	static const bool	IsUnknown	= true;
};

template<typename Ttype> struct STypeTrait<TType*> {
	static const bool	IsPointer	= true;
	static const bool	IsInt		= false;
	static const bool	IsFloat		= false;
	static const bool	IsUnknown	= false;
};

template<> struct STypeTrait<int> {
	static const bool	IsPointer	= false;
	static const bool	IsInt		= true;
	static const bool	IsFloat		= false;
	static const bool	IsUnknown	= false;
};

template<> struct STypeTrait<float> {
	static const bool	IsPointer	= false;
	static const bool	IsInt		= false;
	static const bool	IsFloat		= true;
	static const bool	IsUnknown	= false;
};
					

Auch hier habe ich wieder ein allgemeines Template und Spezialisierungen des selben gebaut. Viel Logik wird hier nicht implementiert. Der einzige Zweck ist mir eine Mechanismus zu erzeugen, mit welchem ich Prüfen kann, von welchem Typ ein Templateparameter ist. Zudem ist es mir jetzt auch möglich, zwischen normalen Datentypen und Pointern zu unterscheiden. Dies ist ganz praktisch, wenn man später mal unterscheiden möchte, ob ein Objekt, oder ein Zeiger auf ein Objekt übergeben wurde, da man bei erstem üder den Punktoperator auf die Member zugreift und bei letzterem über den Pfeiloperator.

Ich werde jetzt das bisherige Beispiel wieder aufgreifen, aber so modifizieren, dass ich genau die eben erstellten Templatestrukturen dafür verwenden kann, um Typprüfungen vorzunehmen.

31
32
33
34
35
36
37
38
39
40
41
42
43
44
					
// Sehr einfacher Datencontainer //////////////////////////////////////////////
template<typename TDataType, unsigned int TSize> class CContainer {
	public:
		typedef TDataType			TType;
		typedef CContainer<TDataType, TSize>	TSelf;

		// Standardkonstruktor ////////////////////////////////////////////
		CContainer(); /////////////////////////////////////////////////////

		// ...

	private:
		TDataType m_aItems[TSize];
}; // CContainer //////////////////////////////////////////////////////////////
					

Die Klassendefinition ist wieder fast die Gleiche, nur dass ich die Defaulttypen für die Templateparameter entfernt habe. Interessant wird jetzt der modifizierte Standardkonstruktor.

48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
					
// Standardkonstruktor ////////////////////////////////////////////////////////
template<typename TDataType, unsigned int TSize>
CContainer<TDataType, TSize>::CContainer() {
	if (STypeTrait<TDataType>::IsUnknown) {
		printf("Nicht implementierter Typ\n");
		return;
	} else if (STypeTrait<TDataType>::IsPointer) {
		printf("Pointer\n");

		// Alles Initialisieren
		for (unsigned int iCount = 0; iCount < TSize; ++iCount) {
			m_aItems[iCount] = NULL;
		} // end of for
	} else if (STypeTrait<TDataType>::IsInt) {
		printf("Integer\n");

		// Alles Initialisieren
		for (unsigned int iCount = 0; iCount < TSize; ++iCount) {
			m_aItems[iCount] = 0;
		} // end of for
	} else if (STypeTrait<TDataType>::IsFloat) {
		printf("Float\n");

		// Alles Initialisieren
		for (unsigned int iCount = 0; iCount < TSize; ++iCount) {
			m_aItems[iCount] = static_cast<TDataType>(0);
		} // end of for
	} // end of if
} // CContainer ///////////////////////////////////////////////////////////////
					

Wie Sie sehen können, kann ich jetzt innerhalb einer Methode auf die verschiedenen Datentypen eingehen und so getrennte Initialisierungen vornehmen. Ganz so einfach ist es dann aber auch nicht, wie Sie in Zeile 73 sehen können. Es ist mir nicht möglich dem Float den Wert "0.0f" zuzuweisen, da sich der Compiler genau an dieser Stelle beschweren wird, wenn man dem Template ein Pointer übergibt. Der Wert null ist aber an dieser Stelle nicht schädlich und bringt nur eine Warnung, die Sie getrost ignorieren können.

Damit Sie sehen, dass der ganze Spaß auch funktioniert, hier noch der Teil für den Aufruf und die Verwendung der modifizierten Klasse.

80
81
82
83
84
85
86
87
88
					
typedef CContainer<double*, 16>	CPoinerContainer;
typedef CContainer<float, 16>	CFloatContainer;
typedef CContainer<int, 16>	CIntContainer;
typedef CContainer<char, 16>	CCharContainer;

CPoinerContainer	oContainer1;
CFloatContainer		oContainer2;
CIntContainer		oContainer3;
CCharContainer		oContainer4;
					

Ausgabe:

Pointer
Float
Integer
Nicht implementierter Typ
		

Wie Sie sehen können, funktioniert diese Lösung perfekt. Falls Sie sich mehr für dieses Thema und die daraus resultierenden Möglichkeiten informieren wollen, schauen Sie einfach im Internet nach. Dort finden Sie unzählige Artikel und Beispiele, sowie ganze Bibliotheken zu diesem Thema.

Abschließend möchte ich noch sagen, dass dies lediglich ein kleiner Vorgeschmack auf dessen ist, was mit Templates alles möglich ist und wie man Sie verwenden kann. Es gibt ganze Bücher zu diesem Thema und deswegen gehe ich nicht weiter darauf ein. Sie sollten lediglich das Grundkonzept verstanden haben, da es auch andere Sprachen gibt (z.B. Java), in welchem Templates möglich sind (wenn auch nicht in dem Umfang wie in C++).

Zum Seitenanfang
Zum Inhaltsverzeichnis

24.5.4 Spezialisierung von Templates

24.5.4 Spezialisierung von Templates

Das eben gezeigte Beispiel hatte unter anderem eine große Schwäche. Der implementierte Container konnte zwar Daten halten, aber nicht sinnvoll initialisieren. Es fehlt also im Standardkonstruktor eine Schleife, in welcher jedem Element ein sinnvoller Startwert zugewiesen wird. Doch welcher Wert ist Sinnvoll? Sicher könnte man im Falle von Integern und Zeigern den Wert null zuweisen, aber schon bei Floats wird es kritisch, denn man müsste "0.0f" angeben. Noch verzwickter wird es, wenn der Container irgendwelche Objekte halten soll. Was können wir also tun?

Die Antwort auf diese Frage ist die Spezialisierung von Templates. Die Grundidee ist, dass man einen allgemein gültigen Fall definiert und dann noch weitere, welche auf spezielle Datentypen eingeht. Jetzt müssten Sie eigentlich mit der Stirn runzeln, denn der Vorteil von Templates ist doch, dass man nur einmal Code schreiben muss und er dann universell einsetzbar ist. Eine erneute Implementierung ist doch genau das Gegenteil. Stellen wir diesen Aspekt aber mal ganz kurz in den Hintergrund und schauen wir uns zunächst das Prinzip an.

Im folgenden Beispiel greife ich das Beispiel von eben auf, wobei ich in der Klassendefinition die Implementierung des Standardkonstruktors entferne (es wird nur noch der Prototyp dastehen) und unterhalb der Klassendefinition mehrfach spezialisiert implementiere. Die Klassendefinition selbst spare ich mir an dieser Stelle.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
					
// Standardkonstruktor ////////////////////////////////////////////////////////
template<typename TDataType, unsigned int TSize>
CContainer<TDataType, TSize>::CContainer() {
	printf("Allgemeines Template\n");
	// keine Initialisierung
} // CContainer ///////////////////////////////////////////////////////////////



template<>
CContainer<float>::CContainer() {
	printf("Spezialisiertes Template 1\n");
	for (unsigned int iCount = 0; iCount < GetSize(); ++iCount) {
		m_aItems[iCount] = 0.0f;
	} // end of for
} // CContainer ///////////////////////////////////////////////////////////////



template<>
CContainer<float, 32>::CContainer() {
	printf("Spezialisiertes Template 2\n");
	for (unsigned int iCount = 0; iCount < 32; ++iCount) {
		m_aItems[iCount] = 0.0f;
	} // end of for
} // CContainer ///////////////////////////////////////////////////////////////



template<>
CContainer<int>::CContainer() {
	printf("Spezialisiertes Template 3\n");
	for (unsigned int iCount = 0; iCount < GetSize(); ++iCount) {
		m_aItems[iCount] = 0;
	} // end of for
} // CContainer ///////////////////////////////////////////////////////////////



// Achtung, jede Templatedefinition mit unterschiedlichen Templateparametern
// bedeutet auch ein unterschiedlicher Datentyp (nicht polymorph)! 
typedef CContainer<float>	CsmallFloatContainer;
typedef CContainer<float, 32>	CbigFloatContainer;
typedef CContainer<int>		CsmallIntContainer;
typedef CContainer<double>	CDoubleContainer;

// ...

CSmallFloatContainer		oContainer1;
CBigFloatContainer		oContainer2;
CSmallIntContainer		oContainer3;
CPoinerContainer		oContainer4;
					

Ausgabe

Spezialisiertes Template 1
Spezialisiertes Template 2
Spezialisiertes Template 3
Allgemeines Template
		

Wie Sie sehen können, habe ich den Standardkonstruktor jetzt mehrfach definiert. Ich überlade ihn aber nicht, sondern ich definiere ihn für verschiedene Szenarien. Zu Beginn, also in den Zeilen 2 bis 6, muss immer die allgemeine Form dastehen, in welcher der allgemeine Fall abgehandelt wird. Da man allgemein nicht sagen kann, wie ein Wert initialisiert werden muss, passiert hier nicht wirklich etwas.

Jetzt fängt es an spannend zu werden. Die nachfolgenden Implementierungen sehen jetzt anders aus. Schauen wir uns zuerst Zeile 10 an. Hier wird wieder das Template angegeben, zu welchem die Methode gehört, aber seltsamerweise habe ich jetzt keine Templateparameter angegeben. Immer dann, wenn Sie so etwas sehen, handelt es sich um eine Spezialisierung eines Templates. Warum muss ich hier keinen Templatetyp angeben? Die Antwort folgt in Zeile 11. Hier folgt jetzt der Methodenkopf und hier wird in den spitzen Klammern ein Primitivtyp angegeben. Somit habe ich aus dem allgemeinen Fall "TDataType" den speziellen Fall "float" definiert. Auch in den Zeilen 12 bis 16 taucht der Type "TDataType" nicht mehr auf. Sicher fragen Sie sich jetzt, was mit "TSize" geworden ist. Da es sich um keinen Templatetyp handelt (viel mehr um eine Art Konstante), braucht man hier nichts anzugeben. Allerdings benötigt ich die Größe dann doch, wenn ich in der for Schleife die Werte initialisieren will. Da "TSize" in diesem Kontext nicht definiert ist, kann ich es nicht verwenden. Somit braucht die Klasse also einen Getter, um genau diesen Wert zu liefern.

Schauen wir uns nun Zeile 20 bis 26 an. Auf den ersten Blick passiert hier das Gleiche, aber bei näherem Betrachten entdeckt man in Zeile 21 einen Unterschied. Dieses mal spezialisiere ich nicht nur den Templatetypen, sondern auch den Primitivtypen. In dem Moment, in welchem ich so etwas mache, kann ich diesen konstanten Wert auch innerhalb der Methode benutzen und somit benötigt ich nicht mehr die Methode "GetSize".

In den Zeilen 30 bis 36 sehen Sie eine Spezialisierung für den Templatetypen Integer. Da ich hier wieder keinen Primitivtypen angegeben habe, muss ich wieder auf den klasseninternen Getter zurückgreifen.

In den Zeilen 42 bis 45 sehen Sie erneute Typendefinitionen. Sie ähneln denen aus dem letzten Beispiel und sollen noch einmal verdeutlichen, dass es sich immer um andere Typen handelt, wenn man das Template mit anderen Parametern benutzt.

Abschließend sehen Sie dann, dass ich lediglich verschiedene Objekte erzeuge, wobei die unterschiedlichen Spezialisierungen genutzt werden. Anhand der Ausgabe sehen Sie, welche Spezialisierung genau benutzt wird. Nur für den Fall, dass keiner der Templateparameter den spezialisierten entspricht, wird die allgemeine Definition benutzt.

Zum Seitenanfang
Zum Inhaltsverzeichnis

24.5.3 Typdefinitionen innerhalb von Templates

24.5.3 Typdefinitionen innerhalb von Templates

Gerade bei Templateklassen macht es Sinn, zu Beginn öffentliche Typdefinitionen zu deklarieren, um sich später den Aufruf bzw. die Deklaration von Variablen, zu vereinfachen und übersichtlicher zu gestalten. Folgendes Beispiel soll dies demonstrieren.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
					
// Sehr einfacher Datencontainer //////////////////////////////////////////////
template<typename TDataType = float, unsigned int TSize = 16>
class CContainer {
	public:
		typedef TDataType			TType;	// Für externen Gebrauch
		typedef CContainer<TDataType, TSize>	TSelf;	// Für internen Gebrauch



		// Standardkonstruktor ////////////////////////////////////////////////
		CContainer() {} ///////////////////////////////////////////////////////

		// Kopierkonstruktor //////////////////////////////////////////////////
		CContainer(TSelf& oContainer) {
			for (unsigned int iCount = 0; iCount < TSize; ++iCount) {
				m_aItems[iCount] = oContainer.m_aItems[iCount];
			} // end of for
		} // CContainer ///////////////////////////////////////////////////////

		// Setzt den Wert im Container an einer gewünschten Position //////////
		inline void SetValue(unsigned int iIndex, TDataType tValue) {
			// Wenn der Index gültig ist
			if (iIndex < TSize)	m_aItems[iIndex] = tValue;
			else			throw "Invalid Index";
		} // GetValue /////////////////////////////////////////////////////////

		// Holt den Wert an einer gewünschten Position aus dem Container //////
		inline TDataType GetValue(unsigned int iIndex) {
			// Wenn der Index gültig ist
			if (iIndex < TSize)	return m_aItems[iIndex];
			else			throw "Invalid Index";
		} // GetValue /////////////////////////////////////////////////////////

		// Gibt die Größe des Containers zurück ///////////////////////////////
		inline unsigned int GetSize() { return TSize; } ///////////////////////

	private:
		TDataType m_aItems[TSize];
};

// ...

// Achtung, jede Templatedefinition mit unterschiedlichen Templateparametern
// bedeutet auch ein unterschiedlicher Datentyp (nicht polymorph)! 
typedef CContainer<>		CSmallFloatContainer;	// float, 16
typedef CContainer<int>		CSmallIntContainer;	// int, 16
typedef CContainer<float, 128>	CBigFloatContainer;	// float, 128

// ...

CSmallFloatContainer		oContainer1;
CSmallFloatContainer::TType	fWert = 12.3f;
oContainer1.SetValue(0, fWert);

CSmallFloatContainer 		oContainer2(oContainer1);
fWert = oContainer2.GetValue(0);

printf("%i %g\n", oContainer2.GetSize(), fWert);
					

Ausgabe:

16 12.3
		

Zunächst ist zu sagen, dass ich mich der Übersichtlichkeit halber für die Inlineschreibweise entschieden habe. Zudem spart es Platz und richtet den Blick mehr aufs wesentliche. Das obige Beispiel implementiert eine Templateklasse, welches ein statisches Array kapselt. Zudem habe ich gleich in Zeile 2 festgelegt, dass es standardmäßig 16 Floats aufnehmen kann.

In den Zeilen 5 und 6 sehen Sie nun besagt öffentliche Typdefinitionen. Die erste ist sehr praktisch für den Zugriff von außen, wohingegen die zweite nützlich für den internen Zugriff ist, wie Sie in Zeile 14 sehen. Dort habe ich einen Kopierkonstruktor definiert. Ihm muss man ja immer eine Referenz auf ein Objekt gleichen Typs übergeben. Im Falle einer Templateklasse bedeutet dies, dass nicht nur der Klassenname ausreicht, sondern dass man auch hier wieder die spitzen Klammern und die Templateparameterdefinition benötigt. Durch die vorhergehende Typdefinition, also dem Alias, schafft man einen wesentlich schöneren Ausdruck und man läuft nicht Gefahr, dass man einen Parameter vergisst.

Der Rest der Klasse ist nicht weiter spannend. Ich habe lediglich einen Setter und zwei Getter eingebaut, welche das Holen bzw. Ändern der Werte ermöglichen.

Spannender sind dann wieder die Zeilen 45 bis 47. Erneut habe ich ein paar Typdefinitionen vorgenommen, um die Deklaration der Container zu vereinfachen. Besonders wichtig an dieser Stelle ist, dass jede Deklaration einen anderen Typ zur Folge hat. Es ist also nicht möglich, einen Container zu erzeugen, welcher Integer - und Floatcontainer aufnimmt. Es sind einfach unterschiedliche Typen/Klassen und ein Array kann immer nur einen Typ bzw. eine Klasse aufnehmen. Selbst ein Container mit 16 Floats und ein anderer mit 17 Floats, stellen unterschiedliche Typen/Klassen dar!

In Zeile 52 sehen Sie jetzt, wofür die erste öffentliche Typdefinition innerhalb der Templateklasse gut ist. In diesem Beispiel ist es zwar offensichtlich, dass man sich hier eine Variable vom Typ Float anlegen muss, aber die gezeigte Methode ist sicherer. Würde man beispielsweise einen anderen Container mit Integern erzeugen (also einfach den Templateparameter ändern), bräuchte man nicht noch diese Variable zu ändern (und ggf. auch nicht alle anderen im Programm). Man ist also wesentlich flexibler und typsicherer.

Der Rest ist dann nicht weiter spannend. Ich erzeuge einfach einen Container, packe eine Zahl rein, erzeuge mir eine Kopie des ersten Kontainers und hole dort den kopierten Wert wieder raus. Anschließend wird er ausgegeben.

Ich hoffe, ich konnte Ihnen ein weiteres mal die Deklaration von eigenen Typdefinitionen schmackhaft machen und den Umgang mit Templates erleichtern.

Zum Seitenanfang
Zum Inhaltsverzeichnis

24.5.2 Primitivtypen als Templateparameter

24.5.2 Primitivtypen als Templateparameter

Bisher hatte ich nur erwähnt, dass es möglich ist, den Templates Typen zu übergeben, welche dann innerhalb des Templates generisch verwendet werden können. Man kann aber auch einfach Werte übergeben. Sie haben dann eine ähnliche Wirkung wie Konstanten. Im folgenden Beispiel erstelle ich ein Template zum erzeugen eines Arrays mit einem generischen Datentyp und gewünschter Größe.

 1
 2
 3
 4
 5
 6
 7
 8
 9
					
// Erzeugt ein generisches Array //////////////////////////////////////////////
template<typename TArrayType = int, unsigned int TSize = 16>
TArrayType* Create() {
	return new TArrayType[TSize];
} // Create ///////////////////////////////////////////////////////////////////

// ...

int* aList = Create<>();	// Erzeugt ein Array mit 10 Integern
					

Wie Sie sehr schön erkennen können kann man auch für diese Primitivtypen Standardwerte festlegen und somit einen sehr kurzen Ausdruck für das Erzeugen des Arrays verwenden. Zugegeben, dieses Beispiel macht nicht viel Sinn. Zum einen braucht man das Erzeugen nicht in ein Template zu verpacken und dann könnte man der Funktion auch die Größe als Parameter übergeben. Anders sieht es da aber schon aus, wenn man ein statisches Array erzeugen wollte. In dem Fall benötigt man einen konstanten Wert, der schon zur Compilezeit feststeht. Genau dies erreicht man mit dieser Art Templateparametern. Wichtig ist bei diesem Szenario, dass man im Aufruf der Templatefunktion auch einen konstanten Wert benutzt.

Natürlich funktioniert dieser Mechanismus auch bei Templateklassen und in der Tat macht er auch bei jenen mehr Sinn (z.B. für die Implementierung einer Vektorklasse).

Zum Seitenanfang
Zum Inhaltsverzeichnis

24.2 Nachteile

24.2 Nachteile

Ich habe auch schon erwähnt, dass man sich mit Templates ein paar Nachteile mit an Bord holt. Das trivialste ist, dass die Implementierung des Templates ein wenig Übung erfordert, weil man eine etwas merkwürdige Syntax benutzen muss. Auch der Umgang mit dem Template erfordert ein wenig seltsamen Code, was dem allgemeinen Verständnis des Programms nicht beiträgt. Sie sollten also erst Templates selber nutzen, wenn Sie wirklich fit darin sind. Das gleiche gilt für das Modifizieren von Templates auf eigene Bedürfnisse.

Ein weiterer klarer Nachteil ist, dass Templates erst vom Compiler übersetzt werden, wenn man sie benutzt. Tut man dies nicht, werden auch keine Fehler erkannt. Daraus ergibt sich auch der Nachteil, dass man den vom Compiler erzeugten tatsächlichen Quelltext, nicht zu Gesicht bekommt, was zur Folge hat, dass man entsprechenden Abschnitt etwas schwieriger Debuggen kann. Zudem erhöht man die Compilezeiten, was bei größeren Projekten einen spürbaren Unterschied ausmacht.

Der größte Nachteil ist, dass man kein Informationhiding vornehmen kann. Damit ist gemeint, dass man normalerweise anderen Entwicklern nur die Header-Datei im Klartext und die zugehörigen vorkompilierten CPP Dateien (als Objektcode), zur Verfügung stellen braucht. Somit sehen andere nicht, wie man die eine oder andere Sache gelöst hat (was ja gut sein kann, wenn es um Firmengeheimnisse geht). Bei Templates hat man ein Problem. Da es sich ja um eine Kopiervorlage handelt, muss sie im Textformat vorliegen und man hat nicht viele Chancen, bestimmte Algorithmen zu tarnen. Dieses Problem relativiert sich aber, da man Templates eh nicht für sehr komplexe Sachen benutzt, sondern eher für Listen usw. und bei denen gibt es nicht viel zu verstecken. Mit Hilfe einer Spezialisierung kann man dann doch wieder Quelltexte verbergen, aber dazu später mehr.

Zum Seitenanfang
Zum Inhaltsverzeichnis

24.4 Klassentemplates

24.4 Klassentemplates

Da Sie nun wisst, wie man Funktionstemplates baut, werde ich jetzt noch eins drauf legen und über Klassentemplates sprechen. Doch bevor es richtig zur Sache geht, muss ich noch ein paar Dinge erklären.

Zum Seitenanfang
Zum Inhaltsverzeichnis

24.4.1 Hinweise

24.4.1 Hinweise

Bevor man sich ein Template baut (egal ob Funktionstemplate oder Klassentemplate), empfehle ich, die Klasse oder Funktion ganz normal zu bauen und erst danach ein Template daraus zu machen. Somit kann man sich erst einmal ganz formal auf die eigentliche Funktionalität konzentrieren. Erst wenn alles funktioniert, sollten Sie aus dem Spezialfall, ein allgemeines Template machen.

Templates bieten außerdem die Möglichkeit, Defaultwerte zu definieren. Damit kann man sich diverse Konstanten im Programm sparen. Wie so etwas aussieht, werde ich dann auch gleich zeigen.

Ein weiteres sehr nettes Feature von Templates ist, dass man sie spezialisieren kann. Damit ist gemeint, dass man zwar ein allgemeines Template bauen kann, aber gewisse Methoden für spezielle Datentypen, anders definieren kann. Was damit gemeint ist, werde ich dann auch noch zeigen.

Zum Seitenanfang
Zum Inhaltsverzeichnis

24.4.2 Die Vorbereitung eines Templates

24.4.2 Die Vorbereitung eines Templates

Wie eben erwähnt, sollte man sich erst einmal eine konkrete Klasse bauen, bevor man dann ein Template daraus macht. Deswegen sehen Sie hier zunächst eine ganz normale Stackklasse, welche Float Werte aufnehmen kann. Schauen wir uns also zuerst die Header-Datei an.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
					
#pragma once

// Stackklasse, welche Floats aufnehmen kann
class CFloatStack {
	private:
		struct SStackElement {
			float		fValue;
			SStackElement*	pNext;
		};

		// Zeiger auf das Oberste Element
		SStackElement*		m_pStackPointer;

	public:
		// Initialisieren
		CFloatStack();
		// Freigeben
		~CFloatStack();

		// Etwas auf den Stack legen
		void	Append(float fItem);
		// Etwas vom Stack runter nehmen
		float	Remove();
};
					

Der Einfachheit halber, habe ich lediglich die zwei zusätzlichen Methoden "Append", zum Hinzufügen von Elementen und "Remove", zum Entfernen von Elementen, eingebaut. Sicher könnte man noch weitere Methoden einbauen, um z.B. alle Elemente auf dem Stack auszugeben, aber darum soll es jetzt nicht gehen. Schauen Sie sich nun die Implementierung in der zugehörigen CPP Datei an.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
					
#include <stdlib.h>
#include "FloatStack.h"



// Initialisieren /////////////////////////////////////////////////////////////
CFloatStack::CFloatStack() 
	: m_pStackPointer(NULL)
{} // CFloatStack /////////////////////////////////////////////////////////////



// Freigeben //////////////////////////////////////////////////////////////////
CFloatStack::~CFloatStack() {
	// Solange noch etwas auf dem Stack ist
	while (this->m_pStackPointer != NULL) this->Remove();
} // ~CFloatStack /////////////////////////////////////////////////////////////



// Etwas auf den Stack legen //////////////////////////////////////////////////
void CFloatStack::Append(float fItem) {
	SStackElement* pElement	= new SStackElement();
	pElement->fValue	= fItem;
	pElement->pNext		= this->m_pStackPointer;
	this->m_pStackPointer	= pElement;
} // Append ///////////////////////////////////////////////////////////////////



// Etwas vom Stack runter nehmen //////////////////////////////////////////////
float CFloatStack::Remove() {
	// Nur wenn was auf dem Stack liegt
	if (this->m_pStackPointer != NULL) {
		SStackElement* pElement	= this->m_pStackPointer;
		this->m_pStackPointer	= pElement->pNext;
		float fReturn		= pElement->fValue;
		pElement->pNext		= NULL;
		delete pElement;

		return fReturn;
	} else {
		return 0.0f;
	} // end of if
} // Remove ///////////////////////////////////////////////////////////////////
					

Bitte verzeihen Sie mir, dass ich hier etwas sparsam mit den Kommentaren war, aber auf die Funktion der Klasse, kommt es hier weniger an. Damit Sie sehen, dass es funktioniert, zeige ich noch kurz, wie die Implementierung in der "main" aussehen könnte.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
					
#include <stdio.h>
#include "FloatStack.h"



// Hauptfunktion der Anwendung ////////////////////////////////////////////////
int main (int argc, char** argv) {
	CFloatStack oStack;
	oStack.Append(3.7f);
	oStack.Append(4.6f);

	printf("%g\n", oStack.Remove());
	printf("%g\n", oStack.Remove());

	return 0;
} // main /////////////////////////////////////////////////////////////////////
					

Ausgabe:

4.6
3.7
		

Jetzt, da die Klasse gebaut und getestet wurde, kann ich mich daran machen, aus ihr ein Template zu bauen.

Zum Seitenanfang
Zum Inhaltsverzeichnis

24.4.3 Das Umwandeln in ein Template

24.4.3 Das Umwandeln in ein Template

Bevor Sie mit der Umwandlung anfangen können, sollten Sie sich etwas bewusst machen. Ich erwähnte ja bereits, dass man mit Templates Schwierigkeiten mit dem Informationhiding hat, weil man ja quasi eine Kopiervorlage baut. Jeder Quelltext muss also ersichtlich sein. Dies gilt natürlich auch für die CPP Datei. Dies hat zur Folge, dass es nicht mehr reicht, die Header-Datei in der "main" zu inkludieren, weil man nicht nur die reinen Funktionsprototypen braucht, sondern auch die Methodenimplementierungen (jene sollen ja durch den Compiler generiert werden).

Jetzt haben Sie also drei Möglichkeiten.

  1. Sie inkludieren in der "main" die H und die CPP Datei oder
  2. Sie inkludieren die CPP Datei am Ende der H Datei oder
  3. Sie definieren die Methoden in der H Datei als Inline-Methode.

Ich entscheide mich für eine Mischung aus Variante 2 und 3, da es zum einen komisch aussieht bzw. sehr unüblich ist, eine CPP Datei in der "main" zu inkludieren und ich zum anderen Zeigen möchte, wie man eine Templatemethode in einer anderen Datei implementiert, da dies alles andere als nahe liegend ist. Ich werde also den Konstruktor, DestruKtor "inline" implementieren und die Methoden "Append" und "Remove" auslagern. Schauen wir uns also zuerst die neue Header-Datei an, aber ich warne Sie jetzt schon, denn es wird ziehmlich merkwürdig / lustig.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
					
#pragma once



// Klassentemplate für eine Stackklasse die beliebige Elemente aufnehmen kann
template<typename TDataType>
class CStack {
	private:
		struct SStackElement {
			TDataType	tValue;
			SStackElement*	pNext;
		};

		// Zeiger auf das Oberste Element
		SStackElement*		m_pStackPointer;

	public:
		// Initialisieren
		inline CStack() : m_pStackPointer(NULL)	{}

		// Freigeben
		inline ~CStack() {
			while (this->m_pStackPointer != NULL) this->Remove();
		}

		// Etwas auf den Stack legen
		void		Append(TDataType tItem);
		// Etwas vom Stack runter nehmen
		TDataType	Remove();
};

#include "Stack.cpp"
					

Gleich in Zeile 6 und 7 sehen Sie die Templatedefinition für die Klasse. Auch sie beginnt wieder mit dem Schlüsselwort "template". Bis auf die Tatsache, dass ich jetzt eine Klasse definiert habe, gibt es keine weiteren Unterschiede zur Definition eines Funktionstemplates. Wie Sie sehen, wurde aus jedem "float" der neu definierte und später zu ersetzende Datentyp "TDataType" gemacht.

Ab Zeile 19 bis 26 wird jetzt der Konstruktor und Destruktor als Inline-Methode deklariert und ihr zugehörige Funktionsweise implementiert. Bis auf das Schlüsselwort "inline", welches noch nicht einmal notwendig ist, passiert hier nichts besonderes. Es wurde lediglich der Methodenprototyp, durch die komplette Methodenimplementierung ersetzt und auch von der Syntax her, sollte alles klar sein.

In Zeile 29 und 31 stehen dann ganz normal die Prototypen der Methoden, welche ich außerhalb des Templates, also in einer anderen Datei, implementieren werde. Wichtig ist aber die Zeile 33. Hier inkludiere ich jetzt die entsprechende CPP Datei, was eigentlich sehr unüblich, aber für meine Herangehensweise notwendig ist. Das "Include" muss auch unbedingt am Ende der Header-Datei stehen. Aber wenn Sie jetzt glauben, dass dies schon merkwürdig war, dann schauen Sie sich folgenden Quelltext an, welchen ich in die CPP Datei geschrieben habe.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
					
#ifndef STACK_CPP
	#define STACK_CPP
	#include <stdlib.h>
	#include "Stack.h"
	
	
	// Etwas auf den Stack legen //////////////////////////////////////////////
	template<typename TDataType>
	void CStack<TDataType>::Append(TDataType tItem) {
		SStackElement* pElement	= new SStackElement();
		pElement->tValue	= tItem;
		pElement->pNext		= this->m_pStackPointer;
		this->m_pStackPointer	= pElement;
	} // Append ///////////////////////////////////////////////////////////////
	
	
	
	// Etwas vom Stack runter nehmen //////////////////////////////////////////
	template<typename TDataType>
	TDataType CStack<TDataType>::Remove() {
		// Nur wenn was auf dem Stack liegt
		if (this->m_pStackPointer != NULL) {
			SStackElement* pElement	= this->m_pStackPointer;
			this->m_pStackPointer	= pElement->pNext;
			TDataType tReturn	= pElement->tValue;
			pElement->pNext		= NULL;
			delete pElement;
	
			return tReturn;
		} else {
			return (TDataType)0;
		} // end of if
	} // Remove ///////////////////////////////////////////////////////////////
#endif
					

Die erste Merkwürdigkeit sollte hier das "Include" der Header-Datei sein, da ich ja in der Header-Datei, bereits die CPP Datei inkludiert habe. Auf den ersten Blick sollte man meinen, dass man jetzt eine gegenseitige Abhängigkeit geschaffen hat und sich die Dateien jetzt im Kreis inkludieren. Das bringt mich auch gleich zur zweiten Merkwürdigkeit. Normalerweise wäre es auch so, dass sich die Dateien im Kreis inkludieren, aber die CPP Datei besitzt jetzt ihren eigenen Includewächter, wie Sie dies sonst nur aus Header-Dateien kennen. Gemeint ist dieses "#ifndef" und anschließende "#define" in Zeile 1 und 2. Wichtig ist hier, dass man nicht den Präprozessorbefehl "#pragma once" benutzen darf, da jener die komplette Datei inkludiert oder nicht. Das Include der Header-Datei ist aber unbedingt erforderlich und nur die reinen Methoden dürfen nicht doppelt vorkommen. Das "Include" der Header-Datei ist im Zweifelsfall auch nicht kritisch, da sich jene selbst vor doppelten Import schützt.

Aber so richtig Merkwürdig wird es dann in Zeile 8 und 9 und spätestens dort, sollten Ihnen die Nackenhaare zu Berge stehen. Was wird hier gemacht? Zunächst wird wieder rein Formal das Template definiert mit seiner Parameterliste. Dies ist notwendig, um zu definieren, dass kommender Abschnitt, mit zu dem bereits definiertem Template gehört. Dies muss dann auch für jede Methode gemacht werden, wie Sie dies auch in Zeile 19 und 20 sehen. Anschließend fängt man jetzt an, den Methodenheader zu definieren. Es geht los mit dem Rückgabetyp. Anschließend folgt der Klassennamen als Namespace, um die Zugehörigkeit zur Klasse festzulegen. Allerdings handelte es sich um eine Templateklasse, was zur Folge hat, dass man nach dem Klassennamen eine spitze Klammer öffnen muss und den definierten Ersetzungstyp übergibt (ggf. auch mehrere, falls mehrere definiert wurden). Hier benötigt man allerdings nur die Parameter, welche die Methode benötigt. Dann schließt man die Spitze Klammer, setzt den doppelten Doppelpunkt und schreibt den Methodennamen, mit den zugehörigen Parametern der Methode in runden Klammern. Abschließend schreibt man dann noch, wie gewohnt in geschweiften Klammern, den eigentlichen Quelltext der Methode. Dort passiert jetzt nichts mehr aufregendes. Man ersetzt lediglich die vorher festen Datentypen, durch die neu definierten Templatedatentypen.

Sie sehen also, dass es durchaus möglich ist, die Implementierung der Methoden in eine andere Datei auszulagern, allerdings auf Kosten der Verständlichkeit. Ich empfehle Ihnen also, nur Inline-Methoden, wie ich es beim Konstruktor und Destruktor gemacht habe, zu schreiben. Sie sollteen sich allerdings diesen Abschnitt genau anschauen, da es, gerade in den internen Bibliotheken von Visual Studio, gerne mal in dieser Art gemacht wird und Sie nachvollziehen können sollten, was passiert, wenn Sie mal einen solchen Quelltext sehen.

Abschließend möchte ich noch zeigen, wie man dieses Template in der "main" einbindet.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
					
#include <stdio.h>
#include "Stack.h"



// Hauptfunktion der Anwendung //////////////////////////////////////////////// 
int main (int argc, char** argv) {
	CStack<float> oStack;
	oStack.Append(3.7f);
	oStack.Append(4.6f);

	printf("%g\n", oStack.Remove());
	printf("%g\n", oStack.Remove());

	return 0;
} // main /////////////////////////////////////////////////////////////////////
					

Wie Sie gleich in Zeile 2 sehen können, braucht man nur die Header-Datei der Templateklasse einzubinden. In Zeile 8 wird jetzt ein Objekt der Templateklasse instantiiert und ähnlich wie Sie das bei den Funktionstemplates gesehen haben, benötigt man jetzt nach dem Klassennamen spitze Klammern in welchen man jetzt den gewünschten Datentyp angibt. Sie erhalten also als Resultat, wieder einen Stack mit Floats. Der Rest ist dann wie gehabt.

Die Ausgabe ist wieder, und warum sollte es auch anders sein, die Gleiche und deswegen gebe ich sie auch nicht noch einmal an.

Zum Seitenanfang
Zum Inhaltsverzeichnis

24.1 Vorteile

24.1 Vorteile

Wie bereits erwähnt, kann man sich noch mehr Arbeit ersparen, weil man flexibler ist, aber was meine ich damit? Nun, stellen Sie sich eine Liste vor. Dort haben Sie in der Regel nur die Möglichkeit, Elemente eines Typs aufzunehmen wie z.B. Integer. Falls Sie irgendwo im Programm (oder auch in einem neuen Projekt), solch eine Liste wieder brauchen, allerdings mit Strings, dann haben Sie ein Problem. Normalerweise müsste man den Quelltext der Liste kopieren und auf den neuen Datentyp anzupassen. Mal abgesehen vom Arbeitsaufwand, könnten sich auch wieder diverse Tippfehler einschleichen und auf Dauer ist dies nicht praktikabel.

Ein Lösungsansatz wäre, sich eine Liste aus "void" Pointern zu bauen. Dann könnte man über entsprechendes Typumwandlungen, die gewünschten Elemente in die Liste ein - und aushängen. Aber auch das hat viele Nachteile. Da nun jeder erdenklicher Datentyp in der Liste stehen kann, weiß man später beim Zugriff auf die Liste nicht mehr, was man zurück bekommt. Ein Zurückcasten kann dann also arg in die Hose gehen. Zudem ist man gezwungen Pointer zu benutzen und hat dadurch unnötige Indirektionen.

Ein weiterer Ansatz wäre, sich für alle Arten von Typen eine Klasse zu bauen und jene dann von einer abstrakten Basisklasse erben zu lassen. Tatsächlich wird dies sogar in manchen Sprachen wie Delphi oder Java gemacht. Dort gibt es eine Klasse "TObject" bzw. "object", von der jede Klasse erbt. Aber auch das würde Sie nur bedingt zum Ziel bringen, da man bei einem Zurückcasten, im Zweifelsfall wieder nicht genau weiß, um was es sich für ein Datentyp handelt. Hinzu kommt, dass man dann oftmals virtuelle Methoden braucht und was das für Folgen hat, habe ich bereits ausführlich diskutiert.

Wie können Sie sich also aus diesem Schlamassel heraus winden? Die Antwort sind Templates. Der Nachteil für das ständige anpassen einer funktionierenden Liste auf einen neuen Datentyp, war doch der Schreibaufwand. Genau da kommen die Ihnen Templates entgegen, weil sie ganz grob gesagt, eine Art abstrakte Kopiervorlage sind. Man entwickelt es ganz allgemein und kann es später auf mehrere unterschiedliche Fälle anwenden.

Nun kann man diesen Mechanismus aber nicht nur für Klassen benutzen, sondern auch für Funktionen. Angenommen Sie möchten eine Funktion bauen, die zwei Werte miteinander verrechnet und einen ganz speziellen Ergebnistyp liefern soll. Normalerweise müsste man also wieder für jeden erdenklichen Datentyp eine separate Funktion bauen, die sich gegenseitig überladen (was schon einmal nicht ohne weiteres geht, falls sich die Parameter der Funktion nicht unterscheiden). Auch hier kann über ein Template eine allgemeine "Kopiervorlage" definiert werden. Wie das alles funktioniert, werde ich noch zeigen.

Ich hatte Ihnen zu Beginn mal die Defines gezeigt. Mit Ihnen konnte man auch Quelltextbausteine erzeugen. Der große Nachteil an ihnen ist, dass man im Debug-Modus nicht sieht, was sich hinter einem Define verbirgt und wenn man sogar eine ganze Funktion so implementiert hat, kann man jene nicht debuggen. Templates leisten genau dies und sie können sogar noch weit mehr Aufgabengebiete abdecken. Ab jetzt sollten Sie also vermindert mit Defines arbeiten.

Zum Seitenanfang
Zum Inhaltsverzeichnis

© Copyright by Thomas Weiß, 2009 - 2012