„Template” w odniesieniu do języka C++ to „wzorzec”, a nie „szablon”. (Za: Bjorne Stroustrup, Język C++, PWN).
Przydatnym dla programistów C++ nowym narzędziem są „typy sparametryzowane”, czyli wzorcewzorce. WzorceSą one tak użytecznym narzędziem, że do definicji języka C++ została wprowadzona standardowa biblioteka wzorców (STL, Standard Template Library).
Z tego rozdziału dowiesz się:
· czym są wzorcewzorce i jak ich używać,
· jak tworzyć wzorcewzorce klas,
· jak tworzyć wzorcewzorce funkcji,
· czym jest standardowa biblioteka wzorcówwzorców i jak z niej korzystać.
Pod koniec czternastego rozdziału stworzyliśmy obiekt PartsList i wykorzystaliśmy go do stworzenia katalogu części, klasy PartsCatalog. Jeśli jednak chcielibyśmy na obiekcie PartsList oprzeć listę kotów, pojawiłby się problem: klasa PartsList wie tylko o częściach.
Aby rozwiązać ten problem, moglibyśmy stworzyć klasę bazową List. Wtedy moglibyśmy wyciąć i wkleić większość kodu z klasy PartsList do nowej deklaracji klasy CatsList. Tydzień później, gdybyśmy zechcieli stworzyć listę obiektów Car, musielibyśmy stworzyć nową klasę i znów kopiować i wklejać kod.
Nie jest to dobre rozwiązanie. Z czasem mogłoby się okazać, że klasa List oraz klasy z niej wyprowadzone muszą być rozszerzone. Rozprowadzenie wszystkich zmian po wszystkich powiązanych z nią klasach byłoby koszmarem.
Problem ten rozwiązują wzorce, które po zaadoptowaniu standardu ANSI stały się integralną częścią języka. Podobnie jak wszystkie elementy w C++, są one bardzo elastyczne, a przy tym bezpieczne (pod względem typów).
Wzorce pozwalają na nauczenie kompilatora, w jaki sposób ma tworzyć listę rzeczy dowolnego typu (zamiast tworzenia zestawu list specyficznego typu) — PartsList jest listą części, CatsList jest listą kotów. Listy te różnią się tylko rodzajem przechowywanych w nich rzeczy. W trakcie korzystania z wzorców typ rzeczy zawartej w liście staje się parametrem definicji klasy.
Standardowym komponentem w prawie wszystkich bibliotekach C++ jest klasa tablicy. Jak widzieliśmy w przypadku klasy List, tworzenie osobnych tablic dla liczb całkowitych, liczb zmiennopozycyjnychrzecinkowych czy dla obiektów jest żmudne i nieefektywne. WzorceWzorce umożliwiają zadeklarowanie sparametryzowanej klasy tablicy, a następnie określenie, jaki typ obiektu będzie zawierał każdy jej egzemplarz. Zauważ, że standardowa biblioteka wzorcówwzorców zawiera standardowy zestaw klas kontenerów, obejmujący tablice, listy i tak dalej. W tym rozdziale sami stworzymy klasę kontenera, aby przekonać się, jak działają wzorcewzorce, ale w komercyjnych aplikacjach prawie z pewnością użyjesz klas standardowych (zamiast tworzyć własne).
Tworzenie egzemplarza wzorca ɯznacza tworzenie określonego typu na podstawie tego wzorca. Poszczególne, tworzone aktualnie klasy są nazywane egzemplarzami wzorcawzorca.
Parametryzowane wzorcewzorce pozwalają na tworzenie klasy ogólnej i przekazywanie jej typów jako parametrów, w celu stworzenia konkretnego egzemplarza wzorcawzorca.
Parametryzowany obiekt Array (wzorzecwzorzec dla tablic) deklarujemy, pisząc:
1: template <class T> // deklaruje wzorzecwzorzec i parametr
2: class Array // parametryzowana klasa
3: {
4: public:
5: Array();
6: // w tym miejscu pełna deklaracja klasy
7: };
Słowo kluczowe template (wzorzecwzorzec) jest używane na początku każdej deklaracji i definicji klasy wzorcawzorcowej. Parametry wzorcawzorca występują po słowie kluczowym template. Parametry są rzeczamizmieniane (uaktualniane) w każdym egzemplarzu. Na przykład, w pokazanym tu wzorcuwzorcu tablicy zmienia się typ przechowywanych w niej obiektów. Jeden egzemplarz może być tablicą liczb całkowitych, a inny może być tablicą obiektów typu Animal.
W tym przykładzie zostało użyte słowo kluczowe class, po którym następuje identyfikator T. Słowo kluczowe class wskazuje że ten parametr (identyfikator) jest typem. Identyfikator T jest używany w pozostałej części definicji wzorcawzorca i odnosi się do typu parametryzowanego. W jednym z egzemplarzy tej klasy w miejscu wszystkich identyfikatorów T zostanie podstawiony na przykład typ int, a w innym egzemplarzu zostanie podstawiony typ Cat.
Aby zadeklarować egzemplarze wzorcawzorca typu int i Cat parametryzowanej klasy Array, moglibyśmy napisać:
Array<int> anIntArray;
Array<Cat> aCatArray;
Obiekt anIntArray to obiekt typu tablica liczb całkowitych; obiekt aCatArray to obiekt typu tablica kotów. Od tego momentu możemy używać typu Array<int> wszędzie tam, gdzie normalnie używalibyśmy jakiegoś typu (np. moglibyśmy użyć typu — jakodla wartości zwracanej przez funkcję, jako parametru funkcji i tak dalej). Pełną deklarację tej okrojonej klasy Array przedstawia listing 19.1.
UWAGA Listing 19.1 nie jest kompletnym programem!
Listing 19.1. WzorzecWzorzec klasy tablicy
0: //Listing 19.1 Wzorzec klasy tablicy
1: #include <iostream>
2: using namespace std;
3: const int DefaultSize = 10;
4:
5: template <class T> // deklaruje wzorzec i parametr
6: class Array // parametryzowana klasa
7: {
8: public:
9: // konstruktory
10: Array(int itsSize = DefaultSize);
11: Array(const Array &rhs);
12: ~Array() { delete [] pType; }
13:
14: // operatory
15: Array& operator=(const Array&);
16: T& operator[](int offSet) { return pType[offSet]; }
17:
18: // akcesory
19: int getSize() { return itsSize; }
20:
21: private:
22: T *pType;
23: int itsSize;
24: };
Wynik
Brak. Program nie jest kompletny.
Analiza
Definicja wzorcawzorca rozpoczyna się w linii 5. od słowa kluczowego template, po którym występuje parametr. W tym przypadku parametr jest identyfikowany przez słowo kluczowe class, zaś do reprezentowania parametryzowanego typu został użyty identyfikator T.
Od linii 6. aż do końca wzorcawzorca w linii 24., pozostała część deklaracji wygląda tak samo jak deklaracja każdej innej klasy. Jedyną różnicą jest to, że tam, gdzie normalnie występowałby typ obiektu, występuje identyfikator T. Na przykład, można oczekiwać, że operator[] będzie zwracał referencję do obiektu zawartego w tablicy — rzeczywiście, jest zadeklarowany jako zwracający referencję do T.
Gdy deklarowany jest egzemplarz tablicy liczb całkowitych, dostarczonyto zdefiniowany dla tej tablicy operator= zwróci referencję do liczby całkowitej. Gdy jest deklarowany egzemplarz tablicy obiektów klasy Animal, dostarczonyto zdefiniowany dla tej tablicy operator= zwróci referencję do obiektu Animal.
Słowo Array może być użyte bez kwalifikowania wewnątrz deklaracji klasy. We wszystkich innych miejscach programu do klasy tej trzeba się odwoływać jako do Array<T>. Na przykład, jeśli nie zawpiszemy konstruktora wewnątrz deklaracji klasy, to musimy napisać:
template <class T>
Array<T>::Array(int size):
itsSize = size
{
pType = new T[size];
for (int i = 0; i < size; i++)
pType[i] = 0;
}
Deklaracja w pierwszej linii tego fragmentu kodu jest wymagana do zidentyfikowania typu (class T). Nazwą wzorcawzorca jest Array<T>, a nazwą funkcji jest Array(int size). Pozostała część funkcji jest taka sama, jak w przypadku zwykłej funkcji. Powszechną i zalecaną praktyką jest przetestowanie działania klasy i jej funkcji jako zwykłych deklaracji przed zamienieniem ich wee wzorzecwzorzec. To rozwiązanie upraszcza tworzenie wzorcawzorca, umożliwiając skoncentrowanie się na celu programowania; rozwiązaniae, poprzez stworzenie wzorcawzorca, uogólnienieamy dopiero później.
Pełna implementacja klasy wzorca Array wymaga zaimplementowania konstruktora kopiującegoi, operatora= i tak dalej. Prosty program sterujący, przenaczony do testowania tej klasy wzorcawzorcowej przedstawia listing 19.2.
UWAGA Niektóre starsze kompilatory nie obsługują wzorcówwzorców. WzorceWzorce są jednak częścią standardu ANSI C++ i są obsługiwane w obecnych wersjach kompilatorów wiodących producentów. Jeśli masz bardzo stary kompilator, nie będziesz mógł skompilować i uruchomić przykładowych programów przedstawionych w tym rozdziale. Jednak mimo to, powinieneś go w całości i wrócić do niego później, gdy zdobędziesz nowszy kompilator.
Listing 19.2. Implementacja wzorcawzorca klasy tablicy
0: //Listing 19.2 Implementacja wzorcawzorca klasy tablicy
2:
5: // deklaruje prostą klasę Animal tak, abyśmy
6: // mogli tworzyć tablice obiektów typu Animal
7:
8: class Animal
9: {
10: public:
11: Animal(int);
12: Animal();
13: ~Animal() {}
14: int GetWeight() const { return itsWeight; }
15: void Display() const { std::cout << itsWeight; }
16: private:
17: int itsWeight;
18: };
19:
20: Animal::Animal(int weight):
21: itsWeight(weight)
22: {}
23:
24: Animal::Animal():
25: itsWeight(0)
26: {}
27:
28:
29: template <class T> // deklaruje wzorzecwzorzec i parametr
30: class Array // parametryzowana klasa
31: {
32: public:
33: // konstruktory
34: Array(int itsSize = DefaultSize);
35: Array(const Array &rhs);
36: ~Array() { delete [] pType; }
37:
38: // operatory
39: Array& operator=(const Array&);
40: T& operator[](int offSet) { return pType[offSet]; }
41: const T& operator[](int offSet) const
42: { return pType[offSet]; }
43: // akcesory
44: int GetSize() const { return itsSize; }
45:
46: private:
47: T *pType;
48: int itsSize;
49: };
50:
51: // oraz implementacje...
52:
53: // implementacja konstruktora
54: template <class T>
55: Array<T>::Array(int size):
56: itsSize(size)
57: {
58: pType = new T[size];
59: for (int i = 0; i<size; i++)
60: pType[i] = 0;
61: }
62:
63: // konstruktor kopiującyi
64: template <class T>
65: Array<T>::Array(const Array &rhs)
66: {
67: itsSize = rhs.GetSize();
68: pType = new T[itsSize];
69: for (int i = 0; i<itsSize; i++)
70: pType[i] = rhs[i];
71: }
72:
73: // operator=
74: template <class T>
75: Array<T>& Array<T>::operator=(const Array &rhs)
...
tga430