I-PROGR2-HOME

  1. Einführung

  2. Ein einfaches Template

  3. Spezialisierung von Templates

  4. Defaultwerte für Templates

  5. Konstanten als Parameter

  6. Schlüsselwort 'typename' für Subtypes

  7. Dynamische vs. Statische Polymorphie

    1. Gebundene dynamische Polymorphie

    2. Ungebundene statische Polymorphie

  8. Templates in der Praxis

  9. Testfragen und Aufgaben


I-PROGR2 SS03 - PROGRAMMIEREN2 - Vorlesung mit Übung
Parametrieiserte Klassen - Templates (Teil 1)

 Achtung : Skript gibt den mündlichen Vortrag nicht vollständig wieder !
 Achtung : Skript noch nicht abgeschlossen !!!
                         

AUTHOR: Gerd Döben-Henisch
DATE OF FIRST GENERATION: May-27, 2003
DATE OF LAST CHANGE: June-2, 2003, 00:40h
EMAIL: Gerd Döben-Henisch



1. Einführung


Die Vorlesung folgt der sehr klaren Darstellung von Kap.7 in [JOSUTTIS 1999]. Die angeführten Kodebeispiele sind den öffentlich zugänglichen Quelldateien zu diesem Buch entnommen. Sie wurden für die VL nur geringfügig geändert.


START



2. Ein einfaches Template


In diesem Beispiel wird von einer einfachen Ausnahmebehandlung Gebrauch gemacht, die erst in dernächsten VL ausführlich behandelt werden wird. Es ist aber ein schönes Beispiel für einen Kontext, in dem eine Ausnahmebehandlung ohne die neuen Techniken von C++, wenn überhaupt, dann nur sehr umständlich zu realisieren wäre.

Im Unterschied zu den bisherigen Klassen wird in einem Template der Klasse der Ausdruck


template <typename T>
class Stack {
 

vorangestellt. Dadurch wird die Klasse gewissermassen mit der Typ-Variablen 'T' parametrisiert (statt T könnte auch jede andere Bezeichnung stehen).

In der usage-Datei wird dann gezeigt, wie man für T z.B. 'int' oder 'std::string' einsetzen kann.



g++ -o stack1_usage stack1_usage.cpp

stack1.hpp




//************************************************
//
// Die folgenden Code-Beispiele stammen aus dem Buch:
//  Objektorientiertes Programmieren in C++
//   Ein Tutorial für Ein- und Umsteiger
// von Nicolai Josuttis, Addison-Wesley München, 2001
//
// (C) Copyright Nicolai Josuttis 2001.
// Diese Software darf kopiert, verwendet, modifiziert und verteilt
// werden, sofern diese Copyright-Angabe in allen Kopien vorhanden ist.
// Diese Software wird "so wie sie ist" zur Verfügung gestellt.
// Es gibt keine explizite oder implizite Garantie über ihren Nutzen.
//**********************************************************************
//
// Modifiziert fuer Vorlesung: gerd doeben-henisch
//
//****************************************************************



#include <iostream>
#include <vector>
#include <cstdlib>
#include <string>

namespace Bsp {  // ******** Beginn 
Namensbereich Bsp::

template <typename T>
class Stack {
  private:
    std::vector<T> elems;  // Elemente

  public:
    Stack();               // Konstruktor
    void push(const T&);   // Element einkellern
    T pop();               // Element auskellern
    T top() const;         // oberstes 
Element
};

// Konstruktor
template <typename T>
Stack<T>::Stack ()
{
    // nichts mehr zu tun
}

template <typename T>
void Stack<T>::push (const T& elem)
{
    elems.push_back(elem);    // Kopie einkellern
}

template<typename T>
T Stack<T>::pop ()
{
    if (elems.empty()) {

      throw "Stack<>::pop(): der Stack ist leer"; //ein return EXIT_FAILURE geht hier
                                                  // nicht, da T nicht vom Typ int sein muss 
!!

    }
    T elem = elems.back();    // oberstes Element merken
    elems.pop_back();         // oberstes Element auskellern
    return elem;              // gemerktes oberstes Element 
zurückliefern
}

template <typename T>
T Stack<T>::top () const
{
    if (elems.empty()) {
      throw  "Stack<>::top(): der Stack ist leer"; //ein return EXIT_FAILURE geht hier
                                                  // nicht, da T nicht vom Typ int sein muss 
!!
    }
    return elems.back();      // oberstes 
Element als Kopie zurückliefern
}

}  // ******** Ende Namensbereich Bsp::


stack1_usage.cpp


In dieser Usage-Datei treten die Schlüsselworte 'try' und 'catch' auf; sie gehören zur Ausnahmebehandlung von C++, die in der nächsten VL behandelt wird. Hier nur soviel, dass im try{...}-Block derjenige Kode steht, der ausgeührt werden soll. Hinter dem try-Block befinden sich dann beliebig viele catch()-Aufrufe, die auf solche Ausnahmen reagieren, die in den vorausgehenden Klassen und Funktionen durch das Schlüsselwort 'throw' markiert worden sind.

 try{......
        std::cout << intStack.pop() << std::endl;
         ......
        std::cout << stringStack.pop() << std::endl; 
        std::cout << stringStack.pop() << std::endl; //Fehler, da leer
  }


    catch (const char* msg) {
        std::cerr << "Exception: " << msg << std::endl;
        return EXIT_FAILURE;
    }  



#include "stack1.hpp"

using namespace Bsp;

int main()
{

  try{
        Bsp::Stack<int>         intStack;       // Stack für 
Integer
        Bsp::Stack<std::string> stringStack;    // Stack für 
Strings

        // Integer-Stack manipulieren
        intStack.push(7);
        std::cout << intStack.pop() << std::endl;

        // String-Stack manipulieren
        std::string s = "hallo";
        stringStack.push(s);
        std::cout << stringStack.pop() << std::endl;
        std::cout << stringStack.pop() << std::endl; //Fehler, da leer
  }


    catch (const char* msg) {
        std::cerr << "Exception: " << msg << std::endl;
        return EXIT_FAILURE;
    }

}


START



3. Spezialisierung von Templates


Eine explizite Spezialisierung von Templates erscheint aus praktischer sicht eher überflüssig, da sie ja in jdem Fall möglich ist, doch bietet C++ dazu eine offizielle Möglichkeit, indem der Klassenkopf entsprechend gestaltet wird:

template<>
class Stack<std::string> {

Das Schlüsselwort 'template' wird von einer leeren spitzen Klammer gefolgt, dafür wird eine explizite konkrete Typangabe an den Klassenbezeichner angehängt. Alles andere läuft nach dem gleichen Muster wie bei einem 'normalen' Template; statt der allgemeinen Typangabe 'T' muss man jetzt immer die spezielle Typangabe 'std::string' (in diesem konkreten Beispiel) machen.


stack2.hpp



#include <deque>
#include <string>

namespace Bsp {  // ******** Beginn 
Namensbereich Bsp::

template<>
class Stack<std::string> {
  private:
    std::deque<std::string> elems;    // Elemente

  public:
    Stack() {                      // 
Konstruktor
    }
    void push(const std::string&);
// Element einkellern     std::string pop();              // Element auskellern     std::string top() const;        // oberstes Element
};

void Stack<std::string>::push 
(const std::string& elem)
{
    elems.push_back(elem);    // Kopie einkellern
}

std::string Stack<std::string>::pop ()
{
    if (elems.empty()) {
      cerr << "Stack<std::string>::pop(): der Stack ist leer";
    }
    std::string elem = elems.back();  // oberstes Element merken
    elems.pop_back();                 // oberstes Element 
auskellern
    return elem;                      // gemerktes oberstes Element 
zurückliefern
}

std::string Stack<std::string>::top () const
{
    if (elems.empty()) {
      cerr <<  "Stack<std::string>::top(): der Stack ist leer";
    }
    return elems.back();              // 
oberstes Element als Kopie zurückliefern
}

}  // ******** Ende Namensbereich Bsp::


START



4. Defaultwerte für Templates


Von der Sprache C her kennt man die Möglichkeit, mittels 'typedef' komplexe Typangaben in einfache Typangaben umzuformen. Dies ist auch mit dem schon eingeführten Schlüsselwort 'typename' möglich; man kann damit nicht nur einfach irgendeinen Ausdruck zu einem Typnamen erklären, man kann diese 'Typisierung' auch mit einer Zuweisung verknüpfen:

typename CONT = std::vector

Durch diese Zuweisung wird dem Ausdruck 'CONT' der Ausdruck 'std::vector' als Wert zugewiesen und zugleich zu einer Typangabe erklärt.



stack3.hpp




#include <vector>
#include <iostream>
#include <deque>
#include <cstdlib>

namespace Bsp {  // ******** Beginn 
Namensbereich Bsp::

template <typename T, typename CONT 
= std::vector<T> > 
class Stack {
  private:
    CONT elems;    // Elemente

  public:
    Stack();              // Konstruktor
    void push(const T&);  // Element einkellern
    T pop();              // Element auskellern
    T top() const;        // oberstes 
Element
};

// Konstruktor
template <typename T, typename 
CONT>
Stack<T,CONT>::Stack ()
{
    // nichts mehr zu tun
}

template <typename T, typename 
CONT>
void Stack<T,CONT>::push (const T& elem)
{
    elems.push_back(elem);    // Kopie einkellern
}

template <typename T, typename 
CONT>
T Stack<T,CONT>::pop ()
{
    if (elems.empty()) {

        throw "Stack<>::pop(): der Stack ist leer";
    }
    T elem = elems.back();    // oberstes Element merken
    elems.pop_back();         // oberstes Element auskellern
    return elem;              // gemerktes oberstes Element 
zurückliefern
}

template <typename T, typename 
CONT>
T Stack<T,CONT>::top () const
{
    if (elems.empty()) {
        throw "Stack<>::top(): der Stack ist leer";
    }
    return elems.back();      // oberstes 
Element als Kopie zurückliefern
}

}  // ******** Ende Namensbereich Bsp::


stack3_usage.cpp




#include "stack3.hpp"

using namespace Bsp;

int main()
{

  try{
        Bsp::Stack<int>         intStack;       // Stack für 
Integer
        Bsp::Stack<double,std::deque<double> >
                                dblStack;       // Stack für Gleitkommawerte

        // Integer-Stack manipulieren
        intStack.push(7);
        std::cout << intStack.pop() << std::endl;

        // Gleitkommawerte-Stack manipulieren
        dblStack.push(42.42);
        std::cout << dblStack.pop() << std::endl;
        std::cout << dblStack.pop() << std::endl;
  }


    catch (const char* msg) {
        std::cerr << "Exception: " << msg << std::endl;
        return EXIT_FAILURE;
    }


}


START



5. Konstanten als Parameter


Aus praktischer Sicht interessant ist auch der Fall, dass man einem Template neben allgemeinen Typangaben auch Konstanten übergeben kann (vergleichbar dem Fall 'define CONST value' in C).



stack4.hpp





#include <iostream>
#include <string>
#include <cstdlib>

namespace Bsp {  // ******** Beginn 
Namensbereich Bsp::

template <typename T, int 
MAXSIZE>
class Stack {
  private:
    T elems[MAXSIZE];     // Elemente
    int numElems;         // aktuelle Anzahl eingetragener 
Elemente

  public:
    Stack();              // Konstruktor
    void push(const T&);  // Element einkellern
    T pop();              // Element auskellern
    T top() const;        // oberstes 
Element
};

// Konstruktor
template <typename T, int 
MAXSIZE>
Stack<T,MAXSIZE>::Stack ()
  : numElems(0)    // kein Elemente
{
    // nichts mehr zu tun
}

template <typename T, int 
MAXSIZE>
void Stack<T,MAXSIZE>::push 
(const T& elem)
{
    if (numElems == MAXSIZE) {
        throw "Stack<>::push(): der Stack ist voll";
    }
    elems[numElems] = elem;   // Element eintragen
    ++numElems;               // Anzahl der Elemente erhöhen
}

template<typename T, int MAXSIZE>
T Stack<T,MAXSIZE>::pop ()
{
    if (numElems <= 0) {
        throw "Stack<>::pop(): der Stack ist leer";
    }
    --numElems;               // Anzahl der Elemente herabsetzen
    return elems[numElems];   // bisheriges oberstes Element 
zurückliefern
}

template <typename T, int 
MAXSIZE>
T Stack<T,MAXSIZE>::top () const
{
    if (numElems <= 0) {
        throw "Stack<>::top(): der Stack ist leer";
    }
    return elems[numElems-1];  // oberstes 
Element zurückliefern
}

}  // ******** Ende Namensbereich Bsp::


stack4_usage.cpp




#include "stack4.hpp"
using namespace Bsp;

int main()
{
  try{

        Bsp::Stack<int,20>         int20Stack;     // Stack für 20 
ints
        Bsp::Stack<int,40>         int40Stack;     // Stack für 40 
ints
        Bsp::Stack<std::string,40> stringStack;    // Stack für 40 
Strings

        // Integer-Stack manipulieren
        int20Stack.push(7);
        std::cout << int20Stack.pop() << std::endl;

        // String-Stack manipulieren
        std::string s = "hallo";
        stringStack.push(s);
        std::cout << stringStack.pop() << std::endl;
        std::cout << stringStack.pop() << std::endl; //Fehler, da leer
  }


    catch (const char* msg) {
        std::cerr << "Exception: " << msg << std::endl;
        return EXIT_FAILURE;
    }


}


START



6. Schlüsselwort 'typename' für Subtypes


Das Schlüsselwort typename, das jetzt schon mehrfach benutzt wurde, bietet neben den bisherigen Möglichkeiten der Typisierung auch noch die Möglichkeit, zu einem allgemeinen Typ T --der z.B. für einen Container-Typ wie 'vector stehen kann-- auch noch einen SubType zu definieren, nämlich das zum jeweiligen container gehörige Iterator-Objekt 'iterator' oder 'const_iterator'. Dadurch kann man dann Variablen einführen, die auf diese speziellen Iterator-Typen bezug nehmen. Im nachfolgenden Beispiel wird davon Gebrauch gemacht:

 typename T::const_iterator pos;
 

hier wird die Variable 'pos' als Objekt vom Type 'T::const_iterator' eingeführt und dann entsprechend benutzt.



ausgeben.hpp



  #include <iostream>

// Elemente eines STL-Containers ausgeben
template <typename T>
void ausgeben (const T& menge)
{
    // Anzahl der Elemente ausgeben
    std::cout << "Anzahl Elemente: " << menge.size() 
<< std::endl;

    // Elemente selbst ausgeben
    std::cout << "Elemente: ";
    typename T::const_iterator pos;
    for (pos=menge.begin(); pos!=menge.end(); ++pos) {
        std::cout << *pos << ' ';
    }
    std::cout << std::endl;
}



 
START



7. Dynamische vs. Statische Polymorphie


7.1 Gebundene dynamische Polymorphie


Damit ist der Fall gemeint, dass man verschiedene Funktionen einführen kann, die auf eine bestimmte Basisklasse Bezug nehmen, aber dynamisch sind, d.h. erst während der Laufzeit wird entschieden, welchen konkreten Typen sie aktuell verarbeiten (siehe unten).

Die im nachfolgenden Beispiel benutzte Klasse 'GeoObj' wurde schon in VL10 eingeführt.



  // beliebiges geometrisches Objekt
zeichnen void zeichne (const
GeoObj& obj) {     obj.draw();
}

// Abstand zwischen zwei geometrischen Objekten berechnen
Koord abstand (const GeoObj& x1, const GeoObj& x2) {
    Koord a = x1.position() - x2.position();
    return a.abs();
}

// inhomogene Menge von geometrischen Objekten ausgeben
void ausgeben (const std::vector<GeoObj*>& elems) {
    for (unsigned i=0; i<elems.
size(); ++i) {         elems[i]->draw();     }
}

int main()
{
    Linie l;
    Kreis k, k1, k2;

    zeichne(l);            // zeichne(GeoObj&) => 
Linie::draw()
    zeichne(k);            // zeichne(GeoObj&) => 
Kreis::draw()

    abstand(k1,k2);        // abstand(GeoObj&,GeoObj&)
    abstand(l,k);          // abstand(GeoObj&,GeoObj&)

    std::vector<GeoObj*> menge;  // inhomogene Menge
    menge.push_back(&l);         // Linie einfügen
    menge.push_back(&k);         // Kreis einfügen
    ausgeben(menge);             // Menge ausgeben
}


START



7.2 Ungebundene statische Polymorphie


Im Falle von Templates liegt keine feste Bindung an eine bestimmte Basisklasse vor, dafür wird aber die Zuordnung zur Kompilierzeit vorgenommen wird und daher inhomogene Mengen (siehe oben) nicht mehr möglich sind.





// beliebiges geometrisches Objekt zeichnen
template <typename GeoObj>
void zeichne (const GeoObj& obj)
{
    obj.draw();
}

// Abstand zwischen zwei geometrischen Objekten berechnen
template <typename GeoObj1, typename
GeoObj2> Koord abstand (const GeoObj1& x1, const GeoObj2& x2) {
    Koord a = x1.position() - x2.position();
    return a.abs();
}

// inhomogene Menge von geometrischen Objekten ausgeben
template <typename GeoObj>
void ausgeben (const std::vector<GeoObj>& elems) {
    for (unsigned i=0; i<elems.size(); ++i) {         elems[i].draw();
    }
}

int main()
{
    Linie l;
    Kreis k;
    Kreis k1, k2;

    zeichne(l);            // zeichne<Linie>(GeoObj&) => 
Linie::draw()
    zeichne(k);            // zeichne<Kreis>(GeoObj&) => 
Kreis::draw()

    abstand(k1,k2);        // 
abstand<Kreis,Kreis>(GeoObj&,GeoObj&)
    abstand(l,k);          // 
abstand<Linie,Kreis>(GeoObj&,GeoObj&)

    //std::vector<GeoObj*> menge;   // FEHLER: keine inhomogene Menge möglich
    std::vector<Linie> menge;        // OK: homogene Menge
    menge.push_back(l);              // Linie einfügen
    ausgeben(menge);                 // Menge ausgeben
}


START



8. Templates in der Praxis



// Deklaration des Funktions-Templates max()
template <typename T>
const T& max (const T& a, const T& b);

// Deklaration des Klassen-Templates Stack<>
#include <vector>

namespace Bsp {  // ******** Beginn 
Namensbereich Bsp::

template <typename T>
class Stack {
  private:
    std::vector<T> elems;  // Elemente
  public:
    Stack();               // Konstruktor
    void push(const T&);   // Element einkellern
    T pop();               // Element auskellern
    T top() const;         // oberstes 
Element
};

}  // ******** Ende Namensbereich Bsp::






#include "expl1.hpp"

// Definition des Funktions-Templates max()
template <typename T>
const T& max (const T& a, const T& b) {
    return (a > b ? a : b);
}

// Definition der Funktionen des Klassen-Templates Stack<>

namespace Bsp {  // ******** Beginn
Namensbereich Bsp::

// Konstruktor
template <typename T>
Stack<T>::Stack ()
{
    // nichts mehr zu tun
}

template <typename T>
void Stack<T>::push (const T& elem)
{
    elems.push_back(elem);    // Kopie einkellern
}

template<typename T>
T Stack<T>::pop ()
{
    if (elems.empty()) {
      cerr << "Stack<>::pop(): der Stack ist leer";
    }
    T elem = elems.back();    // oberstes Element merken
    elems.pop_back();         // oberstes Element auskellern
    return elem;              // gemerktes oberstes Element 
zurückliefern
}

template <typename T>
T Stack<T>::top () const
{
    if (elems.empty()) {
      cerr <<< "Stack<>::top(): der Stack ist leer";
    }
    return elems.back();      // oberstes 
Element als Kopie zurückliefern
}

}  // ******** Ende Namensbereich Bsp::






#include <string>
#include "expldef1.hpp"

// benötigte Funktions-Templates explizit instanziieren
template const int& max (const int&, const int&);

// benötigte Klassen-Templates explizit instanziieren
template Bsp::Stack<int>;
template Bsp::Stack<std::string>;





#include <iostream>
#include <string>
#include <cstdlib>
#include "expl1.hpp" int main() {

        Bsp::Stack<int>         intStack;       // Stack für 
Integer
        Bsp::Stack<std::string> stringStack;    // Stack für 
Strings

        // Integer-Stack manipulieren
        intStack.push(7);
        intStack.push(max(intStack.top(),42));  // max() für ints
        std::cout << intStack.pop() << std::endl;

        // String-Stack manipulieren
        std::string s = "hallo";
        stringStack.push(s);
        std::cout << stringStack.pop() << std::endl;
        std::cout << stringStack.pop() << std::endl;

}



#include <string>
#include "expldef1.hpp"

using namespace Bsp;

// benötigte Funktions-Templates explizit instanziieren
template const int& max (const int&, const int&);

// benötigte Funktionen von Stack<> für int explizit instanziieren
template Bsp::Stack<int>::Stack();
template void Bsp::Stack<int>::push (const int&);
template int Bsp::Stack<int>::top () const;
template int Bsp::Stack<int>::pop ();

// benötigte Funktionen von Stack<> für std::string explizit instanziieren
// - top() wird nicht benötigt
template Bsp::Stack<std::string>::Stack();
template void Bsp::Stack<std::string>::push (const 
std::string&);
template std::string Bsp::Stack<std::string>::pop ();


START



9. Aufgaben



START