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 Ala
Wewnę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