C++-Programmierung/ Templates/ Klassentemplates


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.

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:

#include <iostream>

template < typename T1, typename T2, typename T3 >
class Klasse{
public:
    Klasse(T1 t1, T2 t2, T3 t3):
        t1_(t1), t2_(t2), t3_(t3)
    {
        std::cout << t1_ << '\n' << t2_ << '\n' << t3_ << std::endl;
    }

    T1 getFirst(){ return t1_; }

private:
    T1 t1_;
    T2 t2_;
    T3 t3_;
};

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.


int main(){
    // Konkretisierung,                 Initialisierung
    Klasse< double, char, char const* > object(3.141, 'c', "Toll!!!");

    std::cout << "T1 ist: " << object.getFirst() << std::endl;
}

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:

#include <iostream>

template < std::size_t N, typename T >
class Array{
public:
    Array(){
        for(std::size_t i = 0; i < N; ++i){
            values_[i] = i;
        }
    }

    T& operator[](std::size_t i){ return values_[i % N]; }

private:
    T values_[N];
};

int main(){
    typedef Array< 3, int > Array3Int; // Konkretisierung
    Array3Int object;

    for(std::size_t i = 0; i < 5; ++i){
        std::cout << object[i] << std::endl;
    }
    std::cout << std::endl;

    object[4] = 500; // Identisch mit object[1] = 500; wegen 4 % 3 == 1
    for(std::size_t i = 0; i < 3; ++i){
        std::cout << object[i] << std::endl;
    }
}
Ausgabe:
0
1
2
0
1

0
500
2

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

typedef basic_string< char >           string;
typedef basic_string< wchar_t >        wstring;
typedef basic_ostream< char >          ostream;
typedef basic_iofstream< char >        iofstream;
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 angegeben 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.

template < typename T, char = 'N' >
class A{};

template < typename T1 = A< char >, typename T2 = T1*, char N = 'A', typename T3 = A< T1, N > >
class B{};

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

template < typename charT, typename traits = char_traits< charT >, typename Allocator = allocator< charT > > class basic_string;

template < typename T, typename Container = deque< T > > class queue;

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:

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.

#include <iostream>

template < typename T >
struct A{
    A(){ std::cout << "A< T >" << std::endl; }
};

template < typename T >
struct A< T* >{
    A(){ std::cout << "A< T* >" << std::endl; }
}; // Spezialisierung für alle Zeiger

template <>
struct A< int* >{
    A(){ std::cout << "A< int* >" << std::endl; }
}; // Spezialisierung für Zeiger auf int

template <>
struct A< double >{
    A(){ std::cout << "A< double >" << std::endl; }
}; // Spezialisierung für double

template < typename T >
struct A< T const >{
    A(){ std::cout << "A< T const >" << std::endl; }
}; // Spezialisierung für alle Konstanten

template < typename Result, typename T1, typename T2 >
struct A< Result (*)(T1, T2) >{
    A(){ std::cout << "Result (*)(T1, T2)" << std::endl; }
}; // Spezialisierung für Zeiger auf Funktionen mit 2 Parametern

template < typename Result >
struct A< Result (*)(char, double) >{
    A(){ std::cout << "Result (*)(char, double)" << std::endl; }
}; // Spezialisierung für Zeiger auf Funktionen mit den Parametertypen char und double

int main(){
    A< char >   a; // A< T >
    A< int >    b; // A< T >
    A< double > c; // A< double >

    A< char* >   d; // A< T* >
    A< int* >    e; // A< int* >
    A< double* > f; // A< T* >

    A< char const >   g; // A< T const >
    A< int const >    h; // A< T const >
    A< double const > i; // A< T const >

    A< char* const >   j; // A< T const >
    A< int* const >    k; // A< T const >
    A< double* const > l; // A< T const >

    A< char const* >   m; // A< T* >
    A< int const* >    n; // A< T* >
    A< double const* > o; // A< T* >

    A< int  (*)(char, double) >               p; // Result (*)(char, double)
    A< void (*)(long const**const, float[]) > q; // Result (*)(T1, T2)
    A< void (*)(int) >                        r; // A< T* >
}

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.

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:

#include <stdexcept>

template < std::size_t N, typename T >
class Array{
public:
    Array();
    ~Array();

    T& operator[](std::size_t i);
    T& at(std::size_t i);

    static std::size_t get_count();

private:
    T values_[N];

    static std::size_t count;
};

template < std::size_t N, typename T >
Array< N, T >::Array(){
    for(std::size_t i = 0; i < N; ++i){
        values_[i] = i;
    }
    ++count;
}

template < std::size_t N, typename T >
Array< N, T >::~Array(){
    --count;
}

template < std::size_t N, typename T >
T& Array< N, T >::operator[](std::size_t i){
    return values_[i % N];
}

template < std::size_t N, typename T >
T& Array< N, T >::at(std::size_t i){
    if(i >= N) throw std::out_of_range();
    return operator[](i);
}

template < std::size_t N, typename T >
std::size_t Array< N, T >::get_count(){
    return count;
}

template < std::size_t N, typename T >
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.

#include <cstddef> // Für std::size_t

template < std::size_t N, typename T >
class Array{
public:
    Array();

    template < typename Iterator >
    Array(Iterator iter); // Deklaration des Methodentemplates

    T& operator[](std::size_t i);

private:
    T values_[N];
};

template < std::size_t N, typename T >
Array< N, T >::Array(){
    for(std::size_t i = 0; i < N; ++i){
        values_[i] = i;
    }
}

template < std::size_t N, typename T > // Klassentemplate
template < typename Iterator >         // Methodentemplate
Array< N, T >::Array(Iterator iter){   // Definition der Methode
    for(std::size_t i = 0; i < N; ++i){
        values_[i] = *iter++;
    }
}

template < std::size_t N, typename T >
T& Array< N, T >::operator[](std::size_t i){
    return values_[i % N];
}

#include <vector>
#include <list>
#include <iostream>

int main(){
    std::vector< int > a;
    std::list< int > b;
    int c[5];

	for(std::size_t i = 0; i < 5; ++i){
        a.push_back(i);
        b.push_back(i);
		c[i] = i;
	}

    Array< 5, int > a_t(a.begin());
    Array< 5, int > b_t(b.begin());
    Array< 5, int > c_t(c);

	for(std::size_t i = 0; i < 5; ++i){
        std::cout << a_t[i] << ' ' << b_t[i] << ' ' << c_t[i] << std::endl;
	}
}
Ausgabe:
0 0 0
1 1 1
2 2 2
3 3 3
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.