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 T i M 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:
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. }
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 AlaWewnętrzne nazwy typów (w naszym przykładzie były to i dla int i Ss 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> i Stos<int,11> byłyby dwiema zupełnie różnymi klasami, definiującymi dwa całkowicie odrębne typy danych.
T.R. Werner, 21 lutego 2016; 20:17