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.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

© Copyright by Thomas Weiß, 2009 - 2012