21.1 Szablony klas

Na podobnej zasadzie co szablony funkcji można tworzyć szablony całych klas, wraz z konstruktorami, destruktorami, polami i metodami. Składnia jest analogiczna:

       template <typename T, typename M>
       class Klasa {
           // tu uzywamy typow T i M
       };
definiuje szablon klasy Klasa parametryzowany dwoma typami. Jak teraz utworzyć obiekt klasy wygenerowanej z tego wzorca dla konkretnych typów? Nie możemy po prostu napisać
       Klasa x;
bo kompilator nie wiedziałby, jakie typy przypisać parametrom TM w szablonie. Musimy zatem zażądać jawnie utworzenia na podstawie szablonu i skompilowania konkretnej klasy. Robimy to, podobnie jak dla funkcji, podając nazwy konkretnych typów (klasowych lub wbudowanych) jako argumenty dla wzorca, czyli w nawiasach kątowych. O ile dla funkcji mogliśmy tak zrobić, ale często nie musieliśmy, bo na podstawie typów argumentów wywołania kompilator sam mógł wydedukować potrzebne typy dla parametrów szablonu, o tyle dla klas musimy to robić jawnie, na przykład:
       Klasa<double,int> x;
W ten sposób zażądaliśmy wygenerowania kodu klasy na podstawie szablonu Klasa poprzez zamianę wszystkich wystąpień parametru  T na double, a wystąpień parametru  M na int. Utworzona klasa ma nazwę Klasa<double,int>. W dalszym ciągu programu możemy tej nazwy używać: klasa została już wygenerowana i przy następnych pojawieniach się tej nazwy żadna nowa klasa nie będzie już tworzona. Jeśli natomiast pojawi się nazwa szablonu, ale z innymi typami
       Klasa<int,Osoba> z;
to oczywiście utworzona zostanie nowa klasa, nie mająca nic wspólnego z poprzednią (prócz tego, że obie zostały wygenerowane z tego samego szablonu); jej nazwą będzie Klasa<int,Osoba>.

Parametrem wzorca klasy może też być wartość określonego typu. Na przykład

       template <typename T, int size>
       class Klasa {
           // w definicji uzywamy typu 'T'
           // i wartosci calkowitej 'size'
       };
Konkretną wersję takiej klasy otrzymamy na przykład definiując obiekt
       Klasa<Osoba,100> t;
Nazwą tej klasy będzie oczywiście Klasa<Osoba,100>. Zwróćmy uwagę, że podając inny argument szablonu (na przykład 150 zamiast 100), otrzymalibyśmy inną, całkowicie niezależną klasę.

Pewien kłopot sprawia czasem definiowanie poza szablonem klasy metod w nim zadeklarowanych. Definiując taką metodę trzeba, jak pamiętamy, podać jej nazwę kwalifikowaną. Jeśli jest to szablon, to jako nazwę kwalifikowaną klasy podajemy nazwę szablonu z nazwami parametrów szablonu, już bez słowa kluczowego class lub typename:

      1.      template <typename T, int size>
      2.      class Klasa {
      3.          void metoda1() {
      4.              // definicja metody1
      5.          }
      6.          T* metoda2(double); // tylko deklaracja
      7.  
      8.          // ...
      9.      };
     10.  
     11.      //
     12.      // ...
     13.      //
     14.  
     15.      // definicja metody metoda2
     16.      template <typename T, int size>
     17.      T* Klasa<T,size>::metoda2(double x) {
     18.          // cialo definicji
     19.      }
W tym przykładzie metoda metoda1 jest zdefiniowana bezpośrednio wewnątrz szablonu, a metoda metoda2 poza nim. W podobny sposób można poza klasą definiować konstruktory i destruktory.


Rozpatrzmy bardziej praktyczny przykład wzorca klasy opisującej stos (ang. stack) implementowany za pomocą tablicy (dla zachowania przejrzystości bez obsługi błędów). Musimy zatem zaimplementować metody pozwalające na:

Konkretyzacje szablonu Stos będą różnić się tylko typem elementów kładzionych na stos i wymiarem alokowanej na elementy stosu tablicy:

P172: stos.cpp     Szablon stosu realizowanego w postaci tablicy

      1.  #include <iostream>
      2.  #include <string>
      3.  #include <typeinfo>
      4.  using namespace std;
      5.  
      6.  template <typename Data, int size>
      7.  class Stos {
      8.      Data* data;
      9.      int   top;
     10.  public:
     11.      Stos();
     12.      bool empty() const;
     13.      void push(Data);
     14.      Data pop();
     15.      ~Stos();
     16.  };
     17.  
     18.  template <typename Data, int size>
     19.  Stos<Data,size>::Stos() {
     20.      data = new Data[size];
     21.      top = 0;
     22.  }
     23.  
     24.  template <typename Data, int size>
     25.  inline bool Stos<Data,size>::empty() const {
     26.      return top == 0;
     27.  }
     28.  
     29.  template <typename Data, int size>
     30.  inline void Stos<Data,size>::push(Data dat) {
     31.      data[top++] = dat;
     32.  }
     33.  
     34.  template <typename Data, int size>
     35.  inline Data Stos<Data,size>::pop() {
     36.      return data[--top];
     37.  }
     38.  
     39.  template <typename Data, int size>
     40.  inline Stos<Data,size>::~Stos() {
     41.      delete [] data;
     42.  }
     43.  
     44.    // szablon funkcji globalnej
     45.  template <typename Data, int size>
     46.  void oproznij(Stos<Data,size>* p_stos) {
     47.      cout << "Stos typu " << typeid(Data).name() << ": ";
     48.      while ( ! p_stos->empty() ) {
     49.          cout << p_stos->pop() << " ";
     50.      }
     51.      cout << endl;
     52.  }
     53.  
     54.  int main() {
     55.      Stos<int,20> stos_i;
     56.      stos_i.push(11);
     57.      stos_i.push(36);
     58.      stos_i.push(49);
     59.      stos_i.push(92);
     60.  
     61.      Stos<string,15> stos_s;
     62.      stos_s.push("Ala");
     63.      stos_s.push("Ela");
     64.      stos_s.push("Ola");
     65.      stos_s.push("Ula");
     66.  
     67.      oproznij(&stos_i);
     68.      oproznij(&stos_s);
     69.  }

Parametrami szablonu Stos są typ danych Data i liczba całkowita size określająca maksymalny wymiar stosu. Dla przejrzystości opuściliśmy tu obsługę błędów.

Wewnątrz szablonu deklarujemy konstruktor, destruktor i potrzebne metody. W przypadku tak prostym można je było zdefiniować bezpośrednio wewnątrz szablonu, ale w celach dydaktycznych ich definicje następują poza szablonem klasy (linie 18-42). Ponieważ definicje są poza klasą, a funkcje są bardzo proste, kompilator prawdopodobnie będzie umiał je rozwinąć (patrz rozdział o funkcjach rozwijanych ). Dlatego w definicjach szablonów metod użyliśmy modyfikatora inline.

Dla stosu zaimplementowanego za pomocą tablicy o ustalonym rozmiarze powinniśmy jeszcze pomyśleć o obsłudze błędów, jakie mogą pojawić się, gdy próbujemy położyć na pełnym już stosie dodatkowy element lub zdjąć element ze stosu pustego. Dla zachowania przejrzystości w tym programie tego już nie robimy.

W liniach 44-52 definiujemy wzorzec funkcji globalnych oproznij służących do zdjęcia po kolei wszystkich elementów stosu i wydrukowania ich. Parametrem tych funkcji będzie wskaźnik do stosu typu określonego przez pewną klasę konkretną uzyskaną z szablonu Stos. Funkcja korzysta znów z operatora typeid do wyświetlenia nazwy typu argumentu (wyłącznie w celach dydaktycznych).

W funkcji main definiujemy dwa stosy: stos stos_i liczb całkowitych o maksymalnym rozmiarze 20 oraz stos napisów, stos_s, o maksymalnym wymiarze 15. Kładziemy na każdy z nich po parę elementów, a następnie wywołujemy funkcję oproznij (linie 67 i 68). Wydruk jest następujący

    Stos typu i: 92 49 36 11
    Stos typu Ss: Ula Ola Ela Ala
Wewnętrzne nazwy typów (w naszym przykładzie były to  i dla intSs dla string) jak zwykle mogą zależeć od kompilatora.

Zauważmy, że wywołując funkcję oproznij nie podaliśmy argumentów szablonu. Równie dobrze moglibyśmy napisać

       oproznij<int,20>(&stos_i);
       oproznij<string,15>(&stos_s);
Widzimy jednak, że kompilator nie potrzebował tej podpowiedzi, aby ustalić, na podstawie typu argumentu wywołania, odpowiedni typ i wymiar potrzebny do konkretyzacji szablonu funkcji oproznij.

Zwróćmy jeszcze uwagę na wspomniany już uprzednio fakt: do definicji takiego szablonu klasy jak nasz przykładowy Stos należy nie tylko typ danych (u nas Data), ale i rozmiar (u nas size). Wobec tego klasy Stos<int,10>Stos<int,11> byłyby dwiema zupełnie różnymi klasami, definiującymi dwa całkowicie odrębne typy danych.

T.R. Werner, 23 lutego 2019; 23:59