C++-Programmierung/ Templates/ Klassentemplates

Aus Wikibooks
Zur Navigation springen Zur Suche springen


Wie bereits erwähnt, lassen sich auch Klassen als Template (oder Mustervorlage) definieren. Im Gegensatz zu Funktionen ist für Klassen überhaupt keine Überladung möglich. Schließlich hat der Compiler bei Klassen keine Parameter oder ähnliches, anhand dessen er die gemeinte Klasse erkennen könnte. Somit müssen bei der Konkretisierung eines Templatetyps alle Parametertypen bzw. Werte explizit angegeben werden. Die Parameter des Konstruktors können übrigens auch nicht zum Erkennen der Templateparameter herangezogen werden, da es mehrere Konstruktoren geben kann und diese obendrein ihrerseits Funktionstemplates sein können.

Crystal Project Tutorials.png
Syntax:
template < «Templateparameterliste» >
class «Klassenname»{
    // Definition
};

//----

Templateparameterliste: «Listeneintrag», «Templateparameterliste»
Templateparameterliste: «Leere Liste»

Listeneintrag: typename «Platzhaltername»
Listeneintrag: class «Platzhaltername»
Listeneintrag: «Ganzzahliger Typ» «Platzhaltername»
«Nicht-C++-Code», »optional«

Praktische Beispiele[Bearbeiten]

Zunächst werden wir uns ein einfaches Klassentemplate ansehen:

Nuvola-inspired-terminal.svg
 1 #include <iostream>
 2 
 3 template < typename T1, typename T2, typename T3 >
 4 class Klasse{
 5 public:
 6     Klasse(T1 t1, T2 t2, T3 t3):
 7         t1_(t1), t2_(t2), t3_(t3)
 8     {
 9         std::cout << t1_ << '\n' << t2_ << '\n' << t3_ << std::endl;
10     }
11 
12     T1 getFirst(){ return t1_; }
13 
14 private:
15     T1 t1_;
16     T2 t2_;
17     T3 t3_;
18 };

Nun können wir unser Klassentemplate verwenden. Durch die Angabe konkreter Templateparameter konkretisieren wir unseren Typ. Anschließend können wir unsere Variable verwenden, wie jede andere auch.

Nuvola-inspired-terminal.svg


1 int main(){
2     // Konkretisierung,                 Initialisierung
3     Klasse< double, char, char const* > object(3.141, 'c', "Toll!!!");
4 
5     std::cout << "T1 ist: " << object.getFirst() << std::endl;
6 }

Analog zu Funktionstemplates lassen sich natürlich auch bei Klassentemplates neben Typparametern Ganzzahlparameter verwenden. Außerdem kann mittels typedef jederzeit ein neuer Name für einen konkretisierten Templatetyp erzeugt werden. Das nächste Beispiel wird beides demonstrieren:

Nuvola-inspired-terminal.svg
 1 #include <iostream>
 2 
 3 template < std::size_t N, typename T >
 4 class Array{
 5 public:
 6     Array(){
 7         for(std::size_t i = 0; i < N; ++i){
 8             values_[i] = i;
 9         }
10     }
11 
12     T& operator[](std::size_t i){ return values_[i % N]; }
13 
14 private:
15     T values_[N];
16 };
17 
18 int main(){
19     typedef Array< 3, int > Array3Int; // Konkretisierung
20     Array3Int object;
21 
22     for(std::size_t i = 0; i < 5; ++i){
23         std::cout << object[i] << std::endl;
24     }
25     std::cout << std::endl;
26 
27     object[4] = 500; // Identisch mit object[1] = 500; wegen 4 % 3 == 1
28     for(std::size_t i = 0; i < 3; ++i){
29         std::cout << object[i] << std::endl;
30     }
31 }
Crystal Clear app kscreensaver.svg
Ausgabe:
1 0
2 1
3 2
4 0
5 1
6 
7 0
8 500
9 2

Diese typedef-Technik findet auch in der Standardbibliothek viele Anwendungen. Die folgende kurze Liste zeigt einige Beispiele aus der Standardbibliothek:

Nuvola-inspired-terminal.svg
1 typedef basic_string< char >           string;
2 typedef basic_string< wchar_t >        wstring;
3 typedef basic_ostream< char >          ostream;
4 typedef basic_iofstream< char >        iofstream;
5 typedef basic_istringstream< wchar_t > wistringstream;

Standardparameter[Bearbeiten]

Analog zu Standardparametern für Funktionen können auch für Templates Standardparameter angegeben werden. Die Regeln sind dabei ebenfalls analog zu Funktionen: jeder Parameter, der über einen Standard-Typ/-Wert verfügt, muss entweder ganz rechts stehen oder auf der rechten Seite einen Nachbarn besitzen, der ebenfalls über einen Standard-Typ/-Wert verfügt. Somit müssen Parameter, die in den meisten Fällen gleich sind, nicht bei der Konkretisierung des Templates angeben werden. In den Fällen, wo dann doch mal ein anderer Typ/Wert angegeben werden muss, ist dies jedoch weiterhin problemlos möglich. Wichtig ist, dass die Parameter, die links vom Aktuellen stehen, im Standardparameter verwendet werden können.

Nuvola-inspired-terminal.svg
1 template < typename T, char = 'N' >
2 class A{};
3 
4 template < typename T1 = A< char >, typename T2 = T1*, char N = 'A', typename T3 = A< T1, N > >
5 class B{};

Auch von dieser Technik macht die Standardbibliothek reichlich Gebrauch. Einige Beispiele sind:

Nuvola-inspired-terminal.svg
1 template < typename charT, typename traits = char_traits< charT >, typename Allocator = allocator< charT > > class basic_string;
2 
3 template < typename T, typename Container = deque< T > > class queue;
4 
5 template < typename charT, typename traits = char_traits< charT > > class basic_iostream;

Spezialisierung[Bearbeiten]

Klassentemplates lassen sich ebenso spezialisieren wie Funktionstemplates. Sie lassen sich jedoch, wie schon erwähnt, im Gegensatz zu diesen nicht Überladen. Die Syntax für eine Spezialisierung sieht folgendermaßen aus:

Crystal Project Tutorials.png
Syntax:
template < «Templateparameterliste» >
class «Klassenname» < «Konkretisierung» >{
    // Definition
};

//----

Templateparameterliste: «Listeneintrag», «Templateparameterliste»
Templateparameterliste: «Leere Liste»

Listeneintrag: typename «Platzhaltername»
Listeneintrag: class «Platzhaltername»
Listeneintrag: «Ganzzahliger Typ» «Platzhaltername»
«Nicht-C++-Code», »optional«

In der „Templateparameterliste“ können hier wiederum Platzhalter angegeben werden, die dann in der Konkretisierung verwendet werden können. Wenn ein Template spezialisiert wurde, wird die am besten passende Spezialisierung verwendet. Das folgende Beispiel wird dies demonstrieren, indem im Konstruktor je nach Spezialisierung ein anderer Text ausgegeben wird. Statt des Schlüsselwortes class wird in diesem Beispiel struct verwendet, was uns das public: vor dem Konstruktor erspart.

Nuvola-inspired-terminal.svg
 1 #include <iostream>
 2 
 3 template < typename T >
 4 struct A{
 5     A(){ std::cout << "A< T >" << std::endl; }
 6 };
 7 
 8 template < typename T >
 9 struct A< T* >{
10     A(){ std::cout << "A< T* >" << std::endl; }
11 }; // Spezialisierung für alle Zeiger
12 
13 template <>
14 struct A< int* >{
15     A(){ std::cout << "A< int* >" << std::endl; }
16 }; // Spezialisierung für Zeiger auf int
17 
18 template <>
19 struct A< double >{
20     A(){ std::cout << "A< double >" << std::endl; }
21 }; // Spezialisierung für double
22 
23 template < typename T >
24 struct A< T const >{
25     A(){ std::cout << "A< T const >" << std::endl; }
26 }; // Spezialisierung für alle Konstanten
27 
28 template < typename Result, typename T1, typename T2 >
29 struct A< Result (*)(T1, T2) >{
30     A(){ std::cout << "Result (*)(T1, T2)" << std::endl; }
31 }; // Spezialisierung für Zeiger auf Funktionen mit 2 Parametern
32 
33 template < typename Result >
34 struct A< Result (*)(char, double) >{
35     A(){ std::cout << "Result (*)(char, double)" << std::endl; }
36 }; // Spezialisierung für Zeiger auf Funktionen mit den Parametertypen char und double
37 
38 int main(){
39     A< char >   a; // A< T >
40     A< int >    b; // A< T >
41     A< double > c; // A< double >
42 
43     A< char* >   d; // A< T* >
44     A< int* >    e; // A< int* >
45     A< double* > f; // A< T* >
46 
47     A< char const >   g; // A< T const >
48     A< int const >    h; // A< T const >
49     A< double const > i; // A< T const >
50 
51     A< char* const >   j; // A< T const >
52     A< int* const >    k; // A< T const >
53     A< double* const > l; // A< T const >
54 
55     A< char const* >   m; // A< T* >
56     A< int const* >    n; // A< T* >
57     A< double const* > o; // A< T* >
58 
59     A< int  (*)(char, double) >               p; // Result (*)(char, double)
60     A< void (*)(long const**const, float[]) > q; // Result (*)(T1, T2)
61     A< void (*)(int) >                        r; // A< T* >
62 }

Anhand der Ausgabe beim Konstruktoraufruf lässt sich sehr schön erkennen, welche Spezialisierung der Compiler als am passendsten auserkoren hat. Die Kommentare entsprechen der Ausgabe bei der jeweiligen Zeile. Wenn Sie die Beispiele mal selbst genau unter die Lupe nehmen, werden Sie feststellen, dass sich die Wahl des Compilers leicht nachvollziehen lässt. Werden in einer Spezialisierung keine Platzhalter benötigt, wie hier zwei Mal zu sehen, muss dennoch eine leere Parameterliste angegeben werden, damit der Compiler weiß, dass es sich um eine Spezialisierung eines vorhanden Templates handelt.

Ein Beispiel für Spezialisierung in der Standardbibliothek ist vector< bool >. In den meisten Fällen wird es nicht nötig sein, eine Templateklasse zu überladen, sofern Sie diese als Verallgemeinerung einer gewöhnlichen Klasse schreiben. Sie werden jedoch später noch einiges über Template-Meta-Programmierung lernen, und dabei ist Spezialisierung extrem häufig notwendig. Klassen dienen bei dieser Programmiertechnik meist nur als Hilfsmittel, womit sie also nicht im herkömmlichen Sinne verwendet werden. Dies ist jedoch eine fortgeschrittene C++-Technik.

Definition von Membern[Bearbeiten]

Bisher war der einzige Klassenmember, den wir definiert haben, der Konstruktor. Natürlich ergibt es selten Sinn, derartige Templates zu schreiben. Daher wenden wir uns als Nächstes der Deklaration und Definition von einfachen Membervariablen und Methoden zu. Die Deklaration erfolgt wie in einer gewöhnlichen Klasse. Bei unseren Konstruktorbeispielen haben wir bereits gesehen, dass Methoden, wie auch bei gewöhnlichen Klassen, innerhalb der Klassentemplatedefinition gemacht werden können. Für alle Memberdefinitionen außerhalb der Templateklassendefinition gelten etwas andere Regeln, als bei gewöhnlichen Klassen. Da die Templateparameter des Klassentemplates dort nicht zur Verfügung stehen, müssen Sie erneut angegeben werden. Insbesondere muss nicht nur der Klassenname (getrennt durch den Bereichsoperator ::) vor dem Membernamen angegeben werden, sondern eben auch alle Parameter, die zum Klassentemplate gehören.

Crystal Project Tutorials.png
Syntax:
// (statische) Variablen
template < «Templateparameterliste» >
«Typ» «Klassenname»< «Konkretisierung» >::«Membername»(«Wert»);

// Methoden
template < «Templateparameterliste» >
«Rückgabetyp» «Klassenname»< «Konkretisierung» >::«Membername»(«Funktionsparameter»){
    // Definition
}

//----

Templateparameterliste: «Listeneintrag», «Templateparameterliste»
Templateparameterliste: «Leere Liste»

Listeneintrag: typename «Platzhaltername»
Listeneintrag: class «Platzhaltername»
Listeneintrag: «Ganzzahliger Typ» «Platzhaltername»
«Nicht-C++-Code», »optional«

Folgendes Beispiel führt einige Definitionen vor:

Nuvola-inspired-terminal.svg
 1 #include <stdexcept>
 2 
 3 template < std::size_t N, typename T >
 4 class Array{
 5 public:
 6     Array();
 7     ~Array();
 8 
 9     T& operator[](std::size_t i);
10     T& at(std::size_t i);
11 
12     static std::size_t get_count();
13 
14 private:
15     T values_[N];
16 
17     static std::size_t count;
18 };
19 
20 template < std::size_t N, typename T >
21 Array< N, T >::Array(){
22     for(std::size_t i = 0; i < N; ++i){
23         values_[i] = i;
24     }
25     ++count;
26 }
27 
28 template < std::size_t N, typename T >
29 Array< N, T >::~Array(){
30     --count;
31 }
32 
33 template < std::size_t N, typename T >
34 T& Array< N, T >::operator[](std::size_t i){
35     return values_[i % N];
36 }
37 
38 template < std::size_t N, typename T >
39 T& Array< N, T >::at(std::size_t i){
40     if(i >= N) throw std::out_of_range();
41     return operator[](i);
42 }
43 
44 template < std::size_t N, typename T >
45 std::size_t Array< N, T >::get_count(){
46     return count;
47 }
48 
49 template < std::size_t N, typename T >
50 std::size_t Array< N, T >::count = 0; // oder: count(0)

Natürlich können Sie auf diese Weise leicht einzelne Member der Klasse für beliebige Templateargumente spezialisieren. Sie müssen nur die spezialisierten Argumente in den spitzen Klammern nach dem Klassennamen angeben. Genau wie Funktionstemplates dürfen natürlich auch Member Klassentemplates nur in der aktuellen Datei definiert werden, da der Compiler bei einem Template immer die komplette Definition kennen muss.

Templatemember[Bearbeiten]

Nachdem Sie nun Funktionen und Klassen zu Templates gemacht haben, steht noch die Kombination aus beidem aus, denn auch die Methoden von Klassentemplates lassen sich natürlich als Methodentemplates schreiben. Das einzig Neue dabei ist die Definition der Methoden außerhalb der Klasse, bei der beide Templates spezifiziert werden müssen. Im Folgenden sehen Sie ein Klassentemplate, das einen Templatekonstruktor besitzt, um gleich eine ganze Anzahl von Datentypen zur Initialisierung zuzulassen.

Nuvola-inspired-terminal.svg
 1 #include <cstddef> // Für std::size_t
 2 
 3 template < std::size_t N, typename T >
 4 class Array{
 5 public:
 6     Array();
 7 
 8     template < typename Iterator >
 9     Array(Iterator iter); // Deklaration des Methodentemplates
10 
11     T& operator[](std::size_t i);
12 
13 private:
14     T values_[N];
15 };
16 
17 template < std::size_t N, typename T >
18 Array< N, T >::Array(){
19     for(std::size_t i = 0; i < N; ++i){
20         values_[i] = i;
21     }
22 }
23 
24 template < std::size_t N, typename T > // Klassentemplate
25 template < typename Iterator >         // Methodentemplate
26 Array< N, T >::Array(Iterator iter){   // Definition der Methode
27     for(std::size_t i = 0; i < N; ++i){
28         values_[i] = *iter++;
29     }
30 }
31 
32 template < std::size_t N, typename T >
33 T& Array< N, T >::operator[](std::size_t i){
34     return values_[i % N];
35 }
36 
37 #include <vector>
38 #include <list>
39 #include <iostream>
40 
41 int main(){
42     std::vector< int > a;
43     std::list< int > b;
44     int c[5];
45 
46 	for(std::size_t i = 0; i < 5; ++i){
47         a.push_back(i);
48         b.push_back(i);
49 		c[i] = i;
50 	}
51 
52     Array< 5, int > a_t(a.begin());
53     Array< 5, int > b_t(b.begin());
54     Array< 5, int > c_t(c);
55 
56 	for(std::size_t i = 0; i < 5; ++i){
57         std::cout << a_t[i] << ' ' << b_t[i] << ' ' << c_t[i] << std::endl;
58 	}
59 }
Crystal Clear app kscreensaver.svg
Ausgabe:
1 0 0 0
2 1 1 1
3 2 2 2
4 3 3 3
5 4 4 4

Wie Sie sehen, können Sie nun jeden beliebigen Datentyp für die Initialisierung verwenden, für den der Postfix-Increment- und Dereferenzierungsoperator entsprechend überladen ist. Die hier gezeigte Definition ist nicht besonders gut, da keinerlei Bereichsüberprüfung durchgeführt wird. Der Konstruktor muss sich darauf verlassen, das über den Iterator genügend gültige Elemente zur Verfügung stehen, aber es demonstriert sehr schön, welche Möglichkeiten diese Technik bietet.