Często wiele funcji, jakie musimy definiować, jest do siebie bardzo podobnych — różnią się tylko typem argumentów, a czynności, które mają wykonać, są identyczne. Wyobraźmy sobie na przykład funkcję, która z podanej poprzez argument tablicy wybiera element największy. Jeśli chcemy takiej funkcji użyć dla tablicy int ów, dla tablicy liczb typu double, a potem dla tablicy obiektów klasy Osoba (zakładając, że jest w niej zdefiniowana relacja większości), to przyjdzie nam pisać identyczną w zasadzie funkcję trzy razy: typ parametru musi być jawnie zadeklarowany w definicji funkcji, więc nie można użyć tej samej funkcji dla tablicy osób i tablicy liczb. Jeśli okaże się, że potrzebujemy takiej funkcji również dla tablic obiektów klasy Zwierze, to trzeba będzie dodać odpowiednią funkcję i zrekompilować moduł definiujący te funkcje.
I właśnie w takich sytuacjach przychodzą nam z pomocą szablony. Tworzymy wzorzec (czyli szablon), według którego kompilator sam utworzy tyle wersji danej funkcji, ile będzie trzeba; wszystkie będą w zasadzie takie same, ale różnić się będą typami parametrów, wartości zwracanej i/lub zmiennych lokalnych definiowanych wewnątrz funkcji. Można będzie te funkcje wygenerować nawet dla typów, które w chwili pisania szablonu jeszcze w ogóle nie istniały!
Składnię szablonów funkcji omówimy na następującym przykładzie:
1. #include <iostream> 2. #include <typeinfo> 3. using namespace std; 4. 5. template <class T1, typename T2> ➊ 6. int howmany(T1* arr, T2 mn, T2 mx, int size) { ➋ 7. int count = 0; 8. for (int i = 0; i < size; ++i) 9. if (arr[i] > mn && arr[i] < mx) ++count; 10. 11. // test 12. cout << "T1=" << typeid(T1).name() << " " ➌ 13. << "T2=" << typeid(T2).name() << " "; 14. 15. return count; 16. } 17. 18. int main() { 19. double mnd = 0, mxd = 10; 20. int mni = 0, mxi = 10; 21. double tabd[] = {-2, -1, 2, 5, 7, 11}; 22. int tabi[] = {-2, -1, 2, 5, 7, 11}; 23. 24. int ii = howmany(tabi,mni,mxi,6); ➍ 25. cout << "res=" << ii << endl; 26. 27. int id = howmany(tabi,mnd,mxd,6); ➎ 28. cout << "res=" << id << endl; 29. 30. int di = howmany(tabd,mni,mxi,6); ➏ 31. cout << "res=" << di << endl; 32. 33. int dd = howmany(tabd,mnd,mxd,6); ➐ 34. cout << "res=" << dd << endl << endl; 35. 36. int xx = howmany<double,double>(tabd,mni,mxi,6); ➑ 37. cout << "res=" << xx << endl; 38. }
Słowo template (➊) jest tu słowem kluczowym informującym kompilator, że to, co dalej nastąpi, będzie definicją wzorca funkcji (lub klasy), a nie definicją konkretnej funkcji. Po tym słowie, w nawiasach kątowych, występuje lista parametrów formalnych, oddzielonych przecinkami, każdy poprzedzony słowem kluczowym class lub typename – w tym kontekście te dwa słowa są synonimami, ale zalecane jest to drugie. Parametry formalne wzorca mogą mieć dowolne nazwy, choć często używa się w nich dużej litery T — w naszym przykładzie użyliśmy nazw T1 i T2. Teraz następuje definicja samego wzorca funkcji, w której używamy nazw parametrów formalnych (u nas T1 i T2) w miejscach, gdzie wymagana jest nazwa typu – w szczególności którejś z tych nazw moglibyśmy użyć do określenia typu zwracanego. Można też używać wyrażeń określających typy pochodne, jak T1& czy T2*.
Analizując kod wzorca widzimy, że jest to coś w rodzaju definicji funkcji howmany (➋), która pobiera wskaźnik do tablicy elementów typu T1 i zwraca ilość takich elementów tej tablicy, które są większe od mn a mniejsze od mx. Z kolei mn i mx są typu T2. [Instrukcja ➌ nie jest tu potrzebna, ale została dodana, aby podczas wykonania wypisywał się aktualny typ skojarzony z T1 i T2. Więcej na temat operatora typeid powiemy w rozdziale o RTTI .] Oczywiście, kompilator nie może skompilować wzorca jako funkcji, choćby dlatego, że nie ma typów o nazwach T1 czy T2. Zapamięta jednak, że taki wzorzec istnieje.
Jak możemy takiego wzorca użyć? W linii ➍ wywołujemy funkcję howmany podając jako argumenty tablicę int ów oraz zmienne typu int jako mn i mx. Kompilator zauważy, że funkcji howmany w ogóle nie ma, ale jest szablon o tej nazwie. Spróbuje zatem dopasować we wzorcu typy T1 i T2 tak, aby pasowały do typów argumentów wywołania. Zauważy, że jeśli T1 utożsamić z int i T2 również z int, to wywołanie będzie dokładnie pasować do sygnatury szablonu. Dokona zatem podstawienia: T1 zostanie wszędzie w ciele wzorca zastąpione przez int i to samo dla T2. Nazywa się to konkretyzacją (ang. concretization) wzorca. Otrzymana w ten sposób funkcja zostanie skompilowana (i użyta w czasie wykonania); będzie ona miała postać
int howmany(int* arr, int mn, int mx, int size) { // // ... // }Gdyby w dalszej części programu znów pojawiło się wywołanie funkcji howmany z argumentami tego samego typu, to żadna konkretyzacja wzorca już nie zajdzie — teraz odpowiednia funkcja już będzie istnieć.
Przejdźmy teraz do linii ➎. Tu wywołanie nie pasuje dokładnie do już istniejącej wersji funkcji, bo argumenty drugi i trzeci są tym razem typu double. Konkretyzacja zatem zajdzie jeszcze raz: teraz dokładne dopasowanie otrzymamy po utożsamieniu T1 → int oraz T2 → double. A zatem kompilator wygeneruje następne przeciążenie funkcji howmany, tym razem w postaci
int howmany(int* arr, double mn, double mx, int size) { // // ... // }Podobna sytuacja zajdzie przy wywołaniach z linii ➏ i ➐: w pierwszym przypadku dopasowanie będzie odpowiadać utożsamieniu T1 → double oraz T2 → int, natomiast w drugim T1 → double oraz T2 → double.
Zauważmy jeszcze składnię wywołania z linii ➑. Tu jawnie (nawiasy kątowe za nazwą funkcji) zażądaliśmy podstawienia za pierwszy parametr formalny wzorca (T1) typu double i za drugi(T1) również typu double. Taka też wersja funkcji (w tym przypadku już utworzona wcześniej) zostanie użyta, mimo, że argumenty drugi i trzeci są typu int (oczywiście nic złego się nie stanie, bo konwersja int → double jest konwersją standardową i nie powoduje żadnej straty informacji).
Wynik programu
T1=i T2=i res=3 T1=i T2=d res=3 T1=d T2=i res=3 T1=d T2=d res=3 T1=d T2=d res=3pokazuje, że istotnie utworzone zostały cztery przeciążone wersje funkcji howmany. Tym, że typ int został nazwany i, a double to d nie należy się przejmować — są to zależne od kompilatora wewnętrzne nazwy (kody) typów.
Słowo kluczowe class na liście parametrów szablonu może sugerować, że T1 musi odpowiadać typowi zdefiniowanemu za pomocą klasy. Tak jednak nie jest: jak widzieliśmy z tego prostego przykładu, wartościami odpowiadającymi tym parametrom mogą być dowolne typy, również wbudowane. Dlatego bardziej chyba czytelne jest używanie w tym miejscu słowa kluczowego typename.
Przypatrzmy się teraz następującemu programowi:
1. #include <iostream> 2. #include <typeinfo> 3. using namespace std; 4. 5. template <typename T> ➊ 6. T larger(T k1, T k2) { 7. cout << "T=" << typeid(T).name() << " "; ➋ 8. return k1 < k2 ? k2 : k1; 9. } 10. 11. double larger(double k1, double k2) { ➌ 12. cout << "Spec. double "; 13. return k1 < k2 ? k2 : k1; 14. } 15. 16. template<> ➍ 17. short larger<short>(short k1, short k2) { 18. cout << "Spec. short "; 19. return k1 < k2 ? k2 : k1; 20. } 21. 22. template<> 23. long larger<long>(long k1, long k2) = delete; ➎ 24. 25. int main() { 26. short s1 = 4, s2 = 5; 27. 28. cout << larger(1.5,2.5) << endl; ➏ 29. cout << larger(111,222) << endl; ➐ 30. cout << larger('a','d') << endl; ➑ 31. cout << larger<int>(s1,s2) << endl; ➒ 32. cout << larger(30L,50L) << endl; ➓ 33. }
Definiujemy tu szablon funkcji larger (➊). Wzorzec zależy od jednego tylko parametru T. Funkcja opisywana wzorcem pobiera dwa argumenty tego samego typu, oznaczonego jako T, i zwraca przez wartość rezultat też typu T. Sama funkcja jest bardzo prosta: wartością zwracaną jest wartość większego z argumentów. Jak poprzednio, dodaliśmy linię (➋) która wypisuje informację o aktualnym typie skojarzonym z T (za pomocą operatora typeid dostępnego po dołączeniu pliku nagłówkowego typeinfo — patrz rozdział o RTTI ).
Zwróćmy teraz uwagę na funkcję (nie wzorzec) larger typu double (➌). Jej sygnatura dokładnie odpowiada szablonowi po podstawieniu T → double.
Podobnie dostarczamy też specyficznej wersji funkcji larger dla typu short (➍). Tym razem jednak jawnie poinformowaliśmy kompilator, że to co robimy jest konkretyzacją wzorca dla pewnego konkretnego typu (w tym przypadku short). Zauważmy składnię — puste nawiasy kątowe po słowie template i jawne wskazanie typu parametru wzorca przy nazwie funkcji. Ta forma jest bardziej wskazana, bo pozwala kompilatorowi sprawdzić czy to co robimy rzeczywiście jest poprawną składniowo konkretyzacją wzorca.
W końcu linia ➎ deklaruje konkretyzację naszego wzorca dla typu long jako nieistniejącą (=delete zamiast ciała funkcji — jest to konstrukcja wprowadzona w nowym standardzie C++11).
Program się nie kompiluje:
tmpl.cpp: In function ‘int main()’: tmpl.cpp:32:27: error: use of deleted function ‘T larger(T, T) [with T = long int]’Przyczyna leży w ostatniej linii (➓). Funkcje usunięte (deleted) są brane pod uwagę, kiedy wybierana jest najlepsza funkcja kandydująca; tu będzie nią funkcja powstała z konkretyzacji szablonu po podstawieniu T → long, które daje dopasowanie dokładne. Wtedy dopiero kompilator „zda sobie sprawę”, że ta funkcja jest usunięta — żadna inna kandydatura nie będzie już rozważana i kompilacja zakończy sie niepowodzeniem.
Po wykomentowaniu ostatniej linii kompilacja przebiega pomyślnie i wykonanie daje:
Spec. double 2.5 T=i 222 T=c d T=i 5Zwróćmy uwagę na niektóre aspekty tego programu ilustrujące ogólne zasady używania szablonów funkcji:
Przypatrzmy się jeszcze definicji szablonu. W jego treści dla zmiennych k1 i k2 użyte jest porównanie za pomocą operatora '<'. Jeśli te zmienne są typu numerycznego, to oczywiście nie ma kłopotu: dla typów numerycznych operacja porównywania zdefiniowana jest „sama z siebie”. Dla innych typów, zdefiniowanych za pomocą klas przez użytkownika, takie porównanie może nie być określone — użycie takiego typu do konkretyzacji szablonu spowodowałoby błąd kompilacji (choć, jak się przekonamy, nawet dla własnych typów można określić działanie operatora porównania).
Jako przykład na bardziej realistyczne zastosowanie szablonów,
w poniższym programie definiujemy wzorzec
minmaxmed
funkcji
pobierającej tablicę elementów pewnego typu i obliczającą
element maksymalny, minimalny i medianę (taką wartość, od której
połowa elementów tablicy jest mniejsza, a połowa większa). Tak jak
poprzednio, typ elementów musi dopuszczać porównywanie. Minimum
i maksimum zwracane jest poprzez argumenty przekazane przez referencję,
a mediana jako wartość zwracana funkcji. Dodatkowo, również za
pomocą szablonów definiujemy funkcje
pisztab
(do wypisywnia
elementów tablicy),
inssort
(do porządkowania tablicy
metodą sortowania przez wstawianie — insertion sort) oraz
funkcję
test, która wywołuje poprzednie funkcje.
1. #include <iostream> 2. #include <cstring> // memcpy 3. using namespace std; 4. 5. template<typename T> 6. void pisztab(ostream&,const T[],int); 7. 8. template<typename T> 9. void inssort(T[],int); 10. 11. template<typename T> 12. double minmaxmed(const T[],int,T&,T&); 13. 14. template<typename T> 15. void test(T[],int); 16. 17. int main() { 18. cout << "\n===tablica int===" << endl; 19. int tabi[] = {9,7,2,6,6,2,7,9,2,9,5,2}; 20. test(tabi,sizeof(tabi)/sizeof(int)); 21. 22. cout << "\n===tablica double===" << endl; 23. double tabd[] = {9.5,2.5,6,7.5,9,2,5,2.5}; 24. test(tabd,sizeof(tabd)/sizeof(double)); 25. 26. cout << "\n===tablica unsigned===" << endl; 27. unsigned tabu[] = {23,32,12,76,21,45,20,67}; 28. test(tabu,sizeof(tabu)/sizeof(unsigned)); 29. } 30. 31. template<typename T> 32. void test(T tab[],int size) { 33. T min, max; 34. 35. double mediana = minmaxmed(tab,size,min,max); 36. 37. cout << "min = " << min << ", max = " << max 38. << ", mediana = " << mediana << endl; 39. 40. cout << "Tablica oryginalna: "; 41. pisztab(cout, tab, size); 42. 43. inssort(tab, size); 44. 45. cout << "Tablica posortowana: "; 46. pisztab(cout, tab, size); 47. } 48. 49. template<typename T> 50. void pisztab(ostream& str, const T t[], int size) { 51. str << "[ "; 52. for (int i = 0; i < size; ++i) str << t[i] << " "; 53. str << "]" << endl; 54. } 55. 56. template<typename T> 57. void inssort(T a[], int size) { 58. int i, indmin = 0; // wartownik 59. for (i = 1; i < size; ++i) 60. if (a[i] < a[indmin]) indmin = i; 61. if (indmin != 0) { 62. T p = a[0]; 63. a[0] = a[indmin]; 64. a[indmin] = p; 65. } 66. 67. for (i = 2; i < size; ++i) { // sortowanie 68. int j = i; 69. T v = a[i]; 70. while (v < a[j-1]) { 71. a[j] = a[j-1]; 72. j--; 73. } 74. if (i != j ) a[j] = v; 75. } 76. } 77. 78. template<typename T> 79. double minmaxmed(const T t[], int size, T& min, T& max) { 80. T* tab = new T[size]; 81. memcpy(tab,t,size*sizeof(T)); 82. 83. inssort(tab, size); 84. 85. min = tab[0]; 86. max = tab[size-1]; 87. double mediana = size%2 == 0 ? 88. 0.5*(tab[size/2] + tab[size/2-1]) 89. : tab[size/2]; 90. 91. delete [] tab; 92. return mediana; 93. }
Funkcja minmaxmed najpierw tworzy kopię przekazanej tablicy (aby nie zmodyfikować oryginału), sortuje tę kopię, znajduje wtedy łatwo szukane elementy, po czym usuwa roboczą tablicę. Z wydruku
===tablica int=== min = 2, max = 9, mediana = 6 Tablica oryginalna: [ 9 7 2 6 6 2 7 9 2 9 5 2 ] Tablica posortowana: [ 2 2 2 2 5 6 6 7 7 9 9 9 ] ===tablica double=== min = 2, max = 9.5, mediana = 5.5 Tablica oryginalna: [ 9.5 2.5 6 7.5 9 2 5 2.5 ] Tablica posortowana: [ 2 2.5 2.5 5 6 7.5 9 9.5 ] ===tablica unsigned=== min = 12, max = 76, mediana = 27.5 Tablica oryginalna: [ 23 32 12 76 21 45 20 67 ] Tablica posortowana: [ 12 20 21 23 32 45 67 76 ]widzimy, że dzięki szablonom poradziliśmy sobie z tablicami różnych typów. Zauważmy, że ponieważ do kopiowania tablic użyliśmy, choć oczywiście nie musieliśmy, funkcji memcpy, wszystko będzie działać tylko dla typów, dla których kopiowanie obiektów „bit po bicie” daje właściwy rezultat! Zwróćmy uwagę, że wywołania funkcji określonych przez szablony następują tu również wewnątrz innych szablonów: na przykład szablonowa funkcja test wywołuje szablonowe funkcje minmaxmed, inssort i pisztab.
W tym programie szablony, tak jak zwykłe funkcje, najpierw zostały zadeklarowane, a dopiero później zdefiniowane. Zarówno w deklaracji, jak i w definicji trzeba oczywiście użyć słowa template.
T.R. Werner, 21 lutego 2016; 20:17