Zaawansowane CPP/Wykład 1: Szablony I: Różnice pomiędzy wersjami
Matiunreal (dyskusja | edycje) |
|||
(Nie pokazano 160 wersji utworzonych przez 8 użytkowników) | |||
Linia 1: | Linia 1: | ||
==Szablony funkcji== | ==Szablony funkcji== | ||
Linia 6: | Linia 6: | ||
W praktyce programowania często spotykamy się z funkcjami | W praktyce programowania często spotykamy się z funkcjami | ||
(algorytmami) które można zastosować do szerokiej klasy typów i | (algorytmami), które można zastosować do szerokiej klasy typów i | ||
struktur danych. Typowym przykładem jest funkcja obliczająca maksimum | struktur danych. Typowym przykładem jest funkcja obliczająca maksimum | ||
dwu wartości. Ten trywialny aczkolwiek przydatny kod można zapisać np. | dwu wartości. Ten trywialny, aczkolwiek przydatny kod można zapisać np. | ||
w postaci: | w postaci: | ||
Linia 15: | Linia 15: | ||
}; | }; | ||
{{przyklad| | {{kotwica|prz.1.1|}}{{przyklad|1.1||}} | ||
Funkcja <tt>max</tt> wybiera większy z dwu <tt>int</tt>-ów ale widać że kod | Funkcja <tt>max</tt> wybiera większy z dwu <tt>int</tt>-ów, ale widać, że kod | ||
będzie identyczny dla argumentów dowolnego innego typu pod warunkiem | będzie identyczny dla argumentów dowolnego innego typu pod warunkiem, | ||
iż istnieje dla niego operator porównania i konstruktor kopiujący. W | |||
językach programowania z silną kontrolą typów, takich jak C, C++ czy | językach programowania z silną kontrolą typów, takich jak C, C++ czy | ||
Java definiując funkcję musimy jednak podać typy przekazywanych | Java definiując funkcję musimy jednak podać typy przekazywanych | ||
parametrów oraz typ wartości zwracanej. Oznacza to że dla każdego typu | parametrów oraz typ wartości zwracanej. Oznacza to, że dla każdego typu | ||
argumentów musimy definiować nową funkcję <tt>max</tt>: | argumentów musimy definiować nową funkcję <tt>max</tt>: | ||
Linia 28: | Linia 28: | ||
double max(double a,double b) {return (a>b)?a:b;}; | double max(double a,double b) {return (a>b)?a:b;}; | ||
string max(string a,string b) {return (a>b)?a:b;}; | string max(string a,string b) {return (a>b)?a:b;}; | ||
< | <i>skorzystaliśmy tu z dostępnej w C++ możliwości przeładowywania funkcji</i><br> | ||
main() { | main() { | ||
cout<< max(7,5)<<end; | cout<< max(7,5)<<end; | ||
Linia 35: | Linia 35: | ||
} | } | ||
{{przyklad| | {{kotwica|prz.1.2|}}{{przyklad|1.2||}} | ||
Takie powtarzanie kodu poza oczywistym zwiększeniem nakładu pracy ma | Takie powtarzanie kodu, poza oczywistym zwiększeniem nakładu pracy, ma | ||
inne niepożądane efekty związane z trudnością zapewnienia | inne niepożądane efekty, związane z trudnością zapewnienia | ||
synchronizacji kodu każdej z funkcji. Jeśli np. zauważymy błąd w | synchronizacji kodu każdej z funkcji. Jeśli np. zauważymy błąd w | ||
kodzie to musimy go poprawić w kilku miejscach. To samo dotyczy | kodzie, to musimy go poprawić w kilku miejscach. To samo dotyczy | ||
optymalizacji kodu. W powyższym przykładzie kod jest wyjątkowo prosty, | optymalizacji kodu. W powyższym przykładzie kod jest wyjątkowo prosty, | ||
ale taki sam problem dotyczy np. funkcji sortujących. Rozważmy prosty | ale taki sam problem dotyczy np. funkcji sortujących. Rozważmy prosty | ||
Linia 55: | Linia 55: | ||
} | } | ||
Powyższa funkcja sortuje tablicę zawierającą wartości typu <tt>double</tt> | Powyższa funkcja sortuje tablicę zawierającą wartości typu <tt>double</tt>, | ||
ale widać że znów kod będzie identyczny jeśli zamiast <tt>double</tt> | ale widać, że znów kod będzie identyczny, jeśli zamiast <tt>double</tt> | ||
użyjemy dowolnego innego typu którego wartości możemy porównywać za | użyjemy dowolnego innego typu, którego wartości możemy porównywać za | ||
pomocą funkcji <tt>operator>()</tt> i dla którego zdefiniowany jest | pomocą funkcji <tt>operator>()</tt> i dla którego zdefiniowany jest | ||
operator przypisania. Co więcej kod nie zmieni się jeśli zamiast | operator przypisania. Co więcej, kod nie zmieni się jeśli zamiast | ||
tablicy użyjemy dowolnej innej struktury danych umożliwiającej | tablicy użyjemy dowolnej innej struktury danych, umożliwiającej | ||
indeksowany dostęp do swoich składowych np. <tt>std::vector</tt> | indeksowany dostęp do swoich składowych, np. <tt>std::vector</tt> ze | ||
Standardowej Biblioteki Szablonów STL. W tym przypadku kod jest już | Standardowej Biblioteki Szablonów STL. W tym przypadku kod jest już | ||
bardziej skomplikowany i kłopoty związane z jego powielaniem będą | bardziej skomplikowany i kłopoty związane z jego powielaniem będą | ||
większe. Przykłady takie można mnożyć istnieje bowiem wiele takich | większe. Przykłady takie można mnożyć, istnieje bowiem wiele takich | ||
funkcji czy ''algorytmów uogólnionych''. Ich kod może być znacznie | funkcji czy ''algorytmów uogólnionych''. Ich kod może być znacznie | ||
bardziej skomplikowany niż podanych przykładach a zależność od | bardziej skomplikowany niż w podanych przykładach, a zależność od typu | ||
argumentów nie musi ograniczać się do sygnatury ale występować również | argumentów nie musi ograniczać się do sygnatury, ale występować również | ||
we wnętrzu funkcji jak np. w przypadku zmiennej <tt>tmp</tt> w funkcji | we wnętrzu funkcji, jak np. w przypadku zmiennej <tt>tmp</tt> w funkcji | ||
<tt>swap</tt>. Powielanie takiego kodu dla różnych typów parametrów może | <tt>swap</tt>. Powielanie takiego kodu dla różnych typów parametrów może | ||
łatwo prowadzić do błędów, utrudnia ich wykrywanie, a konieczność | łatwo prowadzić do błędów, utrudnia ich wykrywanie, a konieczność | ||
Linia 75: | Linia 75: | ||
===Funkcje uogólnione bez szablonów=== | ===Funkcje uogólnione bez szablonów=== | ||
Jak radzili, a właściwie jak radzą sobie programiści bez | Jak radzili, a właściwie jak radzą sobie programiści bez możliwości | ||
skorzystania z szablonów? | skorzystania z szablonów? | ||
Tradycyjne sposoby rozwiązywania tego typu problemów to między innymi | Tradycyjne sposoby rozwiązywania tego typu problemów to między innymi | ||
makra: | makra: | ||
<Source> | |||
#define max(a,b) ( (a>b)?a:b) ) | |||
lub używanie wskaźników typów ogólnych takich jak <tt>void *</tt> | </Source> | ||
lub używanie wskaźników typów ogólnych, takich jak <tt>void *</tt>, | |||
jak np. w funkcji <tt>qsort</tt> ze standardowej biblioteki C: | jak np. w funkcji <tt>qsort</tt> ze standardowej biblioteki C: | ||
<Source> | |||
void qsort(void *base, size_t nmemb, size_t size, | |||
int(*compar)(const void *, const void *)); | |||
</Source> | |||
Mimo iż użyteczne żadne z | Mimo iż użyteczne, żadne z | ||
tych rozwiązań nie | tych rozwiązań nie może zostać uznane za wystarczająco ogólne i bezpieczne. | ||
Można się również pokusić o próbę rozwiązania tego problemu za pomocą | Można się również pokusić o próbę rozwiązania tego problemu za pomocą | ||
mechanizmów programowania obiektowego. W sumie jest to bardziej | mechanizmów programowania obiektowego. W sumie jest to bardziej | ||
wyrafinowana odmiana rzutowania na <tt>void *</tt>. Polega na | |||
zdefiniowaniu ogólnego typu dla obiektów które mogą być porównywane: | zdefiniowaniu ogólnego typu dla obiektów, które mogą być porównywane: | ||
<Source> | |||
class GreaterThanComparable { | |||
public: | |||
virtual bool operator>(const GreaterThanComparable &) const = 0; | |||
}; | |||
</Source> | |||
następnie zdefiniowaniu funkcji <tt>max</tt> w postaci: | |||
<Source> | |||
const GreaterThanComparable & | |||
max(const GreaterThanComparable &a, | |||
const GreaterThanComparable &b) { | |||
return (a>b)? a:b; | |||
} | |||
</Source> | |||
([[media:Max_oop.cpp| Źródło: max_oop.cpp]]) | |||
i używaniu jej np. w następujący sposób: | |||
<Source> | |||
class Int:public GreaterThanComparable { | |||
int val; | |||
public: Int(int i <nowiki>=</nowiki> 0):val(i) {}; | |||
operator int() {return val;}; | |||
virtual bool | |||
operator>(const GreaterThanComparable &b) const { | |||
return val > static_cast<const Int&>(b).val; | |||
} | |||
}; | |||
main() { | |||
Int a(1),b(2); | |||
Int c; | |||
c = (const Int &)::max(a,b); | |||
cout<<(int)c<<endl; | |||
} | |||
</Source> | |||
([[media:Max_oop.cpp | Źródło: max_oop.cpp]]) | |||
[ | |||
Widać więc wyraźnie że to podejście wymaga sporego nakładu pracy a | Widać więc wyraźnie, że to podejście wymaga sporego nakładu pracy, a | ||
więc w szczególności w przypadku tak prostej funkcji jak <tt>max</tt> jest | więc w szczególności w przypadku tak prostej funkcji jak <tt>max</tt> jest | ||
wysoce niepraktyczne. Ogólnie rzecz biorąc ma ono następujące wady | wysoce niepraktyczne. Ogólnie rzecz biorąc ma ono następujące wady: | ||
# Wymaga dziedziczenia z abstrakcyjnej klasy bazowej <tt>GreaterThanComparable</tt>, czyli może być zastosowane tylko do typów zdefiniowanych przez nas. Inne typy, w tym typy wbudowane, wymagają kopertowania w klasie opakowującej, takiej jak klasa <tt>Int</tt> w powyższym przykładzie. | |||
być funkcją wirtualną, a więc | # Ponieważ potrzebujemy polimorfizmu funkcja <tt>operator>()</tt> musi być funkcją wirtualną, a więc musi być funkcją składową klasy i nie może być typu <tt>inline</tt>. W przypadku tak prostych funkcji niemożność rozwinięcia ich w miejscu wywołania może prowadzić do dużych narzutów w czasie wykonania. | ||
nie może być typu <tt>inline</tt>. W przypadku tak prostych funkcji | # Funkcja <tt>max</tt> zwraca zawsze referencje do <tt>GreaterThanComparable</tt>, więc konieczne jest rzutowanie na typ wynikowy (tu <tt>Int</tt>). | ||
niemożność rozwinięcia ich w miejscu wywołania może prowadzić do | |||
dużych narzutów w czasie wykonania. | |||
więc konieczne jest rzutowanie na typ wynikowy (tu <tt>Int</tt>). | |||
===Szablony funkcji=== | ===Szablony funkcji=== | ||
Widać, że podejście obiektowe nie nadaje się najlepiej do rozwiązywania | Widać, że podejście obiektowe nie nadaje się najlepiej do rozwiązywania | ||
tego szczególnego problemu powielania kodu | tego szczególnego problemu powielania kodu. Dlatego w C++ wprowadzono | ||
nowy mechanizm: szablony. Szablony zezwalają na definiowanie całych | nowy mechanizm: szablony. Szablony zezwalają na definiowanie całych | ||
rodzin funkcji które następnie mogą być używane dla różnych typów | rodzin funkcji, które następnie mogą być używane dla różnych typów | ||
argumentów. | argumentów. | ||
Definicja szablonu funkcji <tt>max</tt> odpowiadającej definicji | Definicja szablonu funkcji <tt>max</tt>, odpowiadającej definicji [[#prz.1.1|1.1]] wygląda następująco: | ||
[[# | |||
<Source> | |||
template<typename T> T max(T a,T b) {return (a>b)?a:b;}; | |||
[ | </Source> | ||
([[media:Max_template.cpp | Źródło: max_template.cpp]]) | |||
Przyjrzyjmy się jej z bliska. Wyrażenie <tt>template<typename T></tt> | Przyjrzyjmy się jej z bliska. Wyrażenie <tt>template<typename T></tt> | ||
oznacza że mamy do czynienia z szablonem który posiada jeden parametr | oznacza, że mamy do czynienia z szablonem, który posiada jeden parametr | ||
formalny nazwany <tt>T</tt>. Słowo kluczowe <tt>typename</tt> oznacza że | formalny nazwany <tt>T</tt>. Słowo kluczowe <tt>typename</tt> oznacza, że | ||
parametr ten jest typem (nazwą typu). Zamiast słowa <tt>typename</tt> | parametr ten jest typem (nazwą typu). Zamiast słowa <tt>typename</tt> | ||
możmy użyć słowa kluczowego <tt>class</tt>. Nazwa tego parametru może być | możmy użyć słowa kluczowego <tt>class</tt>. Nazwa tego parametru może być | ||
następnie wykorzystywana w definicji funkcji w miejscach gdzie | następnie wykorzystywana w definicji funkcji w miejscach, gdzie | ||
spodziewamy się nazwy typu. I tak powyższe wyrażenie definiuje | spodziewamy się nazwy typu. I tak powyższe wyrażenie definiuje funkcję | ||
<tt>max</tt> która przyjmuje dwa argumenty typu <tt>T</tt> i zwraca wartość | <tt>max</tt>, która przyjmuje dwa argumenty typu <tt>T</tt> i zwraca wartość | ||
typu <tt>T</tt> będącą wartością większego z dwu argumentów. Typ <tt>T</tt> | typu <tt>T</tt>, będącą wartością większego z dwu argumentów. Typ <tt>T</tt> | ||
jest na razie | jest na razie niewyspecyfikowany. W tym sensie szablon definiuje całą | ||
rodzinę funkcji. Konkretną funkcję z tej rodziny wybieramy poprzez | rodzinę funkcji. Konkretną funkcję z tej rodziny wybieramy poprzez | ||
podstawienie | podstawienie za formalny parametr <tt>T</tt> konkretnego typu będącego | ||
argumentem szablonu. Takie podstawienie nazywamy konkretyzacją | argumentem szablonu. Takie podstawienie nazywamy konkretyzacją | ||
szablonu. Argument szablonu umieszczamy w nawiasach ostrych za nazwą | szablonu. Argument szablonu umieszczamy w nawiasach ostrych za nazwą | ||
szablonu (w praktyce można uniknąć konieczności jawnej specyfikacji | szablonu (w praktyce można uniknąć konieczności jawnej specyfikacji | ||
argumentów szablonu, opiszemy to w następnych częściach wykładu): | argumentów szablonu, opiszemy to w następnych częściach wykładu): | ||
<Source> | |||
int i,j,k; | |||
k=max<int>(i,j); | |||
</Source> | |||
Takie użycie szablonu spowoduje wygenerowanie identycznej funkcji jak | Takie użycie szablonu spowoduje wygenerowanie identycznej funkcji jak [[#prz.1.1|1.1]]. W powyższym przypadku za <tt>T</tt> podstawiamy <tt>int</tt>. Oczywiście możemy podstawić za <tt>T</tt> dowolny typ i używając szablonów program [[#prz.1.2|1.2]] można zapisać następująco: | ||
[[# | <Source> | ||
[[# | template<typename T> T max(T a,T b) {return (a>b)?a:b;} | ||
main() { | |||
cout<<::max<int>(7,5)<<endl; | |||
cout<<::max<double>(3.1415,2.71)<<endl; | |||
cout<<::max<string>("Ania","Basia")<<endl; | |||
} | |||
</Source> | |||
([[media:Max_template.cpp | Źródło: max_template.cpp]]) | |||
[ | |||
W powyższym kodzie użyliśmy konstrukcji <tt>::max(a,b)</tt>. Dwa dwukropki | W powyższym kodzie użyliśmy konstrukcji <tt>::max(a,b)</tt>. Dwa dwukropki | ||
oznaczają | oznaczają, że używamy funkcji <tt>max</tt> zdefiniowanej w ogólnej | ||
przestrzeni nazw. Jest to konieczne aby kod się skompilował ponieważ | przestrzeni nazw. Jest to konieczne aby kod się skompilował, ponieważ | ||
szablon <tt>max</tt> istnieje już w standardowej przestrzeni nazw | szablon <tt>max</tt> istnieje już w standardowej przestrzeni nazw | ||
<tt>std</tt>. W dalszej części wykładu będziemy te podwójne dwukropki pomijać. | <tt>std</tt>. W dalszej części wykładu będziemy te podwójne dwukropki pomijać. | ||
Oczywiście istnieją typy których | Oczywiście istnieją typy których podstawienie spowoduje błędy | ||
kompilacji, np. | |||
<Source> | |||
complex<double> c1,c2; | |||
max<complex<double> >(c1,c2); //brak operatora > | |||
[ | </Source> | ||
([[media:Max_template.cpp | Źródło: max_template.cpp]]) | |||
lub | lub | ||
<Source> | |||
class X { | |||
private: | |||
X(const X &){}; | |||
}; | |||
X a,b; | |||
max<X>(a,b); //prywatny (niewidoczny) konstruktor kopiujący | |||
</Source> | |||
([[media:Max_template.cpp | Źródło: max_template.cpp]]) | |||
[ | |||
Ogólnie rzecz biorąc każdy szablon definiuje pewną klasę typów które | Ogólnie rzecz biorąc, każdy szablon definiuje pewną klasę typów, które | ||
mogą zostać podstawione jako jego argumenty. | mogą zostać podstawione jako jego argumenty. | ||
===Dedukcja argumentów szablonu=== | ===Dedukcja argumentów szablonu=== | ||
Użyteczność szablonów funkcji zwiększa istotnie fakt że argumenty szablonu nie muszą być podawane jawnie. Kompilator może je wydedukować z argumentów funkcji. Tak więc zamiast | Użyteczność szablonów funkcji zwiększa istotnie fakt, że argumenty szablonu nie muszą być podawane jawnie. Kompilator może je wydedukować z argumentów funkcji. Tak więc zamiast | ||
int i,j,k; | int i,j,k; | ||
Linia 242: | Linia 231: | ||
k<nowiki>=</nowiki>max(i,j); | k<nowiki>=</nowiki>max(i,j); | ||
i kompilator zauważy że tylko podstawienie | i kompilator zauważy, że tylko podstawienie <tt>int</tt>-a za <tt>T</tt> umożliwi | ||
dopasowanie sygnatury funkcji do parametrów jej wywołania i | dopasowanie sygnatury funkcji do parametrów jej wywołania i | ||
automatycznie dokona odpowiedniej konkretyzacji. | automatycznie dokona odpowiedniej konkretyzacji. | ||
Może się zdarzyć że podamy takie argumenty funkcji że dopasowanie | Może się zdarzyć, że podamy takie argumenty funkcji, że dopasowanie | ||
argumentów wzorca będzie niemożliwe, otrzymamy wtedy błąd kompilacji. | argumentów wzorca będzie niemożliwe, otrzymamy wtedy błąd kompilacji. | ||
Trzeba pamiętać że mechanizm automatycznego dopasowywania argumentów | Trzeba pamiętać, że mechanizm automatycznego dopasowywania argumentów | ||
szablonu powoduje wyłączenie automatycznej konwersji argumentów | szablonu powoduje wyłączenie automatycznej konwersji argumentów | ||
funkcji. Podanie jawnie argumentów szablonu (w nawiasach ostrych za | funkcji. Podanie jawnie argumentów szablonu (w nawiasach ostrych za | ||
nazwą szablonu) jednoznacznie określa sygnaturę funkcji a więc | nazwą szablonu) jednoznacznie określa sygnaturę funkcji, a więc | ||
umożliwia automatyczną konwersję typów. Ilustruje to poniższy kod: | umożliwia automatyczną konwersję typów. Ilustruje to poniższy kod: | ||
<Source> | |||
template<typename T> T max(T a,T b) {return (a>b)?a:b;} | |||
main() { | |||
cout<<::max(3.14,2)<<endl; | |||
// błąd: kompilator nie jest w stanie wydedukowac argumentu szablonu, bo typy | |||
// argumentów (double,int) nie pasują do (T,T) | |||
cout<<::max<int>(3.14,2)<<endl; | |||
// podając argument jawnie wymuszamy sygnaturę int max(int,int), a co za tym | |||
// idzie automatyczną konwersję argumentu 1 do int-a | |||
cout<<::max<double>(3.14,2)<<endl; | |||
// podając argument szablonu jawnie wymuszamy sygnaturę | |||
// double max(double,double) | |||
// a co za tym idzie automatyczną konwersję argumentu 2 do double-a | |||
int i; | |||
cout<<::max<int *>(&i,i)<<endl; | |||
//błąd: nie istnieje konwersja z typu int na int* | |||
</Source> | |||
([[media:Max_template.cpp | Źródło: max_template.cpp]]) | |||
Może warto zauważyć, że automatyczna dedukcja parametrów szablonu jest | |||
możliwa tylko wtedy, jeśli parametry wywołania funkcji w jakiś sposób | |||
Może warto zauważyć że automatyczna dedukcja parametrów szablonu jest | |||
możliwa tylko wtedy jeśli parametry wywołania funkcji w jakiś sposób | |||
zależą od parametrów szablonu. Jeśli tej zależności nie ma, z przyczyn | zależą od parametrów szablonu. Jeśli tej zależności nie ma, z przyczyn | ||
oczywistych dedukcja nie jest możliwa i trzeba parametry podawać | oczywistych dedukcja nie jest możliwa i trzeba parametry podawać | ||
jawnie. Wtedy istotna jest kolejność parametrów na liście. Jeżeli | jawnie. Wtedy istotna jest kolejność parametrów na liście. Jeżeli | ||
parametry których nie da się wydedukować umieścimy jako pierwsze, | parametry, których nie da się wydedukować, umieścimy jako pierwsze, | ||
wystarczy że tylko je podamy jawnie a kompilator wydedukuje resztę. | wystarczy, że tylko je podamy jawnie, a kompilator wydedukuje resztę. | ||
Ilustruje to poniższy kod: | Ilustruje to poniższy kod: | ||
Linia 290: | Linia 280: | ||
return (T)u; | return (T)u; | ||
}; | }; | ||
< | <i>fukcje różnią się tylko kolejnością parametrów szablonu</i><br> | ||
main() { | main() { | ||
cout<<convert(33)<<endl; | cout<<convert(33)<<endl; | ||
< | <i>błąd: kompilator nie jest w stanie wydedukować pierwszego parametru | ||
szablonu, bo nie zależy on od parametru wywołania funkcji</i><br> | |||
cout<<convert<char>(33)<<endl; | cout<<convert<char>(33)<<endl; | ||
< | <i>w porządku: podajemy jawnie argument T, kompilator sam dedukuje | ||
argument U z typu argumentu wywołania funkcji</i><br> | |||
cout<<inv_convert<char>('a')<<endl; | cout<<inv_convert<char>('a')<<endl; | ||
< | <i>błąd: podajemy jawnie argument odpowiadający parametrowi U. | ||
Kompilator nie jest w stanie wydedukować argumentu T, bo nie zależy on od argumentu | |||
wywołania funkcji</i><br> | |||
cout<<inv_convert<int,char>(33)<<endl; | cout<<inv_convert<int,char>(33)<<endl; | ||
< | <i>w porządku: podajemy jawnie oba argumenty szablonu</i> | ||
} | } | ||
[ | ([[media:Convert.cpp | Źródło: convert.cpp]]) | ||
===Używanie szablonów=== | ===Używanie szablonów=== | ||
Linia 317: | Linia 303: | ||
przykładach. W językach C i C++ zwykle rozdzielamy deklarację funkcji | przykładach. W językach C i C++ zwykle rozdzielamy deklarację funkcji | ||
od jej definicji i zwyczajowo umieszczamy deklarację w plikach | od jej definicji i zwyczajowo umieszczamy deklarację w plikach | ||
nagłówkowych <tt>*.h</tt> a definicję w plikach źródłowych <tt>*.c</tt>, | nagłówkowych <tt>*.h</tt>, a definicję w plikach źródłowych <tt>*.c</tt>, | ||
<tt>*.cpp</tt> itp. Pliki nagłówkowe są w czasie kompilacji włączane do | <tt>*.cpp</tt> itp. Pliki nagłówkowe są w czasie kompilacji włączane do | ||
plików w których chcemy korzystać z danej funkcji a pliki źródłowe są | plików, w których chcemy korzystać z danej funkcji, a pliki źródłowe są | ||
pojedynczo kompilowane do plików “obiektowych” <tt>*.o</tt>. Następnie | pojedynczo kompilowane do plików “obiektowych” <tt>*.o</tt>. Następnie | ||
pliki obiektowe są łączone w jeden plik wynikowy (zob | pliki obiektowe są łączone w jeden plik wynikowy (zob. [[#rys.1.1|rysunek 1.1]]). W pliku | ||
korzystającym z danej funkcji nie musimy więc znać jej definicji, a | |||
korzystającym z danej funkcji nie musimy więc znać jej definicji a | |||
tylko deklarację. Na podstawie nazwy funkcji konsolidator powiąże | tylko deklarację. Na podstawie nazwy funkcji konsolidator powiąże | ||
wywołanie funkcji z jej implementacją znajdującą się w innym pliku | wywołanie funkcji z jej implementacją znajdującą się w innym pliku | ||
obiektowym. W ten sposób tylko zmiana deklaracji funkcji wymaga | obiektowym. W ten sposób tylko zmiana deklaracji funkcji wymaga | ||
rekompilacji plików w których z niej korzystamy, a zmiana definicji | rekompilacji plików, w których z niej korzystamy, a zmiana definicji | ||
wymaga jedynie rekompilacji pliku w którym dana funkcja jest | wymaga jedynie rekompilacji pliku, w którym dana funkcja jest | ||
zdefiniowana. | zdefiniowana. | ||
{{kotwica|rys.1.1|}}<center> | |||
<div class="thumb"><div style="width:750px;"> | |||
[[Grafika:Cpp-1-kod1.png]] | |||
użycia zwykłych funkcji. | <div.thumbcaption>Rysunek 1.1. Przykład organizacji kodu C++ w przypadku | ||
użycia zwykłych funkcji.</div> | |||
</div></div> | |||
</center> | |||
Taka organizacja umożliwia przestrzeganie "reguły jednej definicji" | Taka organizacja umożliwia przestrzeganie "reguły jednej definicji" | ||
(one definition rule) wymaganej przez C++. | (one definition rule), wymaganej przez C++. | ||
Osobom nieobeznanym z programowaniem w C/C++ zwracam uwagę na | Osobom nieobeznanym z programowaniem w C/C++ zwracam uwagę na konstrukcję | ||
#ifndef _nazwa_pliku_ | |||
#define _nazwa_pliku_ | |||
... | |||
#endif | |||
uniemożliwiajacą podwójne włączenie tego pliku do jednej jednostki | uniemożliwiajacą podwójne włączenie tego pliku do jednej jednostki | ||
translacyjnej. | translacyjnej. | ||
Podobne podejście do kompilacje szablonów się nie powiedzie (zob | Podobne podejście do kompilacje szablonów się nie powiedzie (zob. [[#rys.1.2|rysunek 1.2]]). | ||
Powodem jest fakt, że w trakcie kompilacji pliku <tt>utils.cpp</tt> | |||
Powodem jest fakt że w trakcie kompilacji pliku <tt>utils.cpp</tt> | kompilator nie wie jeszcze, że potrzebna będzie funkcja <tt>max<int></tt>, | ||
kompilator nie wie jeszcze że potrzebna będzie funkcja <tt>max<int></tt>, | wobec czego nie generuje kodu żadnej funkcji, a jedynie sprawdza | ||
wobec czego nie generuje kodu żadnej funkcji a jedynie sprawdza | |||
poprawność gramatyczną szablonu. Z kolei podczas kompilacji pliku | poprawność gramatyczną szablonu. Z kolei podczas kompilacji pliku | ||
main.cpp kompilator już wie że ma skonkretyzować szablon dla T <nowiki>=</nowiki> int | <tt>main.cpp</tt> kompilator już wie, że ma skonkretyzować szablon dla <tt>T <nowiki>=</nowiki> int</tt>, | ||
ale nie ma dostępu do kodu szablonu. | ale nie ma dostępu do kodu szablonu. | ||
{{kotwica|rys.1.2|}}<center> | |||
[[File:cpp-1-kod2.svg|750x500px|thumb|center|Rysunek 1.2. Przykład błędnej organizacji kodu w przypadku | |||
użycia szablonów | użycia szablonów]] | ||
Istnieją różne rozwiązania tego problemu. Najprościej chyba jest | Istnieją różne rozwiązania tego problemu. Najprościej chyba jest | ||
zauważyć że opisane zachowanie jest analogiczne do zachowania podczas | zauważyć, że opisane zachowanie jest analogiczne do zachowania podczas | ||
kompilacji funkcji rozwijanych w miejscu wywołania (<tt>inline</tt>) | kompilacji funkcji rozwijanych w miejscu wywołania (<tt>inline</tt>), | ||
których definicja również musi być dostępna w czasie kompilacji. | których definicja również musi być dostępna w czasie kompilacji. | ||
Podobnie więc jak w tym przypadku możemy zamieścić wszystkie | Podobnie więc jak w tym przypadku możemy zamieścić wszystkie | ||
deklaracje i definicje szablonów w pliknu nagłówkowym | deklaracje i definicje szablonów w pliknu nagłówkowym, włączanym do | ||
plików, w ktorych z tych szablonów korzystamy (zob. [[#rys.1.3|rysunek 1.3]]). Podobnie | |||
jak w przypadku funkcji <tt>inline</tt> reguła jednej definicji zezwala na | jak w przypadku funkcji <tt>inline</tt> reguła jednej definicji zezwala na | ||
powtarzanie definicji/deklaracji szablonów w różnych jednostkach | powtarzanie definicji/deklaracji szablonów w różnych jednostkach | ||
translacyjnych pod warunkiem że są one identyczne. Stąd konieczność | translacyjnych, pod warunkiem, że są one identyczne. Stąd konieczność | ||
umieszczania ich w plikach nagłówkowych. | umieszczania ich w plikach nagłówkowych. | ||
{{kotwica|rys.1.3|}}<center> | |||
[[File:cpp-1-kod3.svg|750x500px|thumb|center|Rysunek 1.3. Przykład organizacji kodu z szablonami, wykorzystującego strategię włączania.]] | |||
</center> | |||
Ten sposób organizacji pracy z szablonami, nazywany modelem włączenia, | |||
jest najbardziej uniwersalny. Jego główną wadą jestkonieczność | |||
Ten sposób organizacji pracy z szablonami, nazywany modelem włączenia | |||
jest najbardziej uniwersalny. Jego główną wadą | |||
rekompilacji całego kodu korzystającego z szablonów przy każdej | rekompilacji całego kodu korzystającego z szablonów przy każdej | ||
zmianie definicji szablonu. Również jeśli zmienimy coś w pliku w | zmianie definicji szablonu. Również jeśli zmienimy coś w pliku, w | ||
którym korzystamy z szablonu to musimy rekompilować cały kod szablonu | którym korzystamy z szablonu, to musimy rekompilować cały kod szablonu | ||
włączony do tego pliku nawet jeśli nie uległ on zmianie. Jeśli się | włączony do tego pliku, nawet jeśli nie uległ on zmianie. Jeśli się | ||
uwzględni fakt że kompilacja szablonu jest bardziej skomplikowana od | uwzględni fakt, że kompilacja szablonu jest bardziej skomplikowana od | ||
kompilacji "zwykłego" kodu | kompilacji "zwykłego" kodu, to duży projekt intensywnie korzystający z | ||
szablonów może wymagać bardzo długich czasów kompilacji. | szablonów może wymagać bardzo długich czasów kompilacji. | ||
Możemy też w jakiś sposób dać znać kompilatorowi że podczas kompilacji | Możemy też w jakiś sposób dać znać kompilatorowi, że podczas kompilacji | ||
pliku <tt>utils.cpp</tt> powinien wygenerować kod dla funkcji | pliku <tt>utils.cpp</tt> powinien wygenerować kod dla funkcji | ||
<tt>max<int></tt>. Można to zrobić dodając jawne żądanie konkretyzacji | <tt>max<int></tt>. Można to zrobić dodając jawne żądanie konkretyzacji | ||
szablonu (zob | szablonu (zob. [[#rys.1.4|rysunek 1.4]]): | ||
template<typename T> T max(T a,T b) {return (a>b)?a:b;} | template<typename T> T max(T a,T b) {return (a>b)?a:b;} | ||
template int max<int>(int ,int) ; | template int max<int>(int ,int) ; <i>konkretyzacja jawna</i> | ||
Używając konkretyzacji jawnej musimy pamiętać o dokonaniu | Używając konkretyzacji jawnej musimy pamiętać o dokonaniu | ||
konkretyzacji | konkretyzacji każdej używanej funkcji, tak że to podejście nie | ||
skaluje zbyt dobrze. Ponadto w przypadku szablonów klas (omawianych w | skaluje się zbyt dobrze. Ponadto w przypadku szablonów klas (omawianych w | ||
następnym module) konkretyzacja jawna pociąga za sobą konkretyzację | następnym module) konkretyzacja jawna pociąga za sobą konkretyzację | ||
wszystkich metod danej klasy a konkretyzacja “na żądanie” jedynie | wszystkich metod danej klasy, a konkretyzacja “na żądanie” - jedynie | ||
tych używanych w programie. | tych używanych w programie. | ||
{{kotwica|rys.1.4|}}<center> | |||
<div class="thumb"><div style="width:804px;"> | |||
[[Grafika:Cpp-1-kod4.png]] | |||
wykorzystującego jawną konkretyzację | <div.thumbcaption>Rysunek 1.4. Przykład organizacji kodu z szablonami, | ||
wykorzystującego jawną konkretyzację.</div> | |||
</div></div> | |||
</center> | |||
===Pozatypowe parametry szablonów === | ===Pozatypowe parametry szablonów === | ||
Poza parametrami określającymi typ, takimi jak parametr | Poza parametrami określającymi typ, takimi jak parametr <tt>T</tt> w | ||
dotychczasowych przykładach szablony funkcji mogą przyjmować również | dotychczasowych przykładach, szablony funkcji mogą przyjmować również | ||
parametry innego rodzaju. Obecnie mogą | parametry innego rodzaju. Obecnie mogą to być inne szablony, co omówię w | ||
następnym podrozdziale lub parametry określające nie typ ale wartości. | następnym podrozdziale lub parametry określające nie typ, ale wartości. | ||
Jak na razie (w obecnym standardzie) te wartości nie mogą być dowolne | Jak na razie (w obecnym standardzie) te wartości nie mogą być dowolne, | ||
ale muszą mieć jeden z poniższych typów: | ale muszą mieć jeden z poniższych typów: | ||
# typ całkowitoliczbowy bądź typ wyliczeniowy | # typ całkowitoliczbowy bądź typ wyliczeniowy | ||
# typ wskaźnikowy | # typ wskaźnikowy | ||
# typ referencyjny. | |||
Takie parametry określające wartość nazywamy parametrami pozatypowymi. | |||
Takie parametry | |||
W praktyce z parametrów pozatypowych najczęściej używa się | W praktyce z parametrów pozatypowych najczęściej używa się | ||
parametrów typu całkowitoliczbowego. Np. | parametrów typu całkowitoliczbowego. Np. | ||
template<size_t N,typename T> T dot_product(T *a,T *b) { | template<size_t N,typename T> T dot_product(T *a,T *b) { | ||
T total<nowiki>=</nowiki>0.0; | |||
for(size_t i<nowiki>=</nowiki>0;i<N;++i) | |||
total +<nowiki>=</nowiki> a[i]*b[i] ;<br> | |||
return total; | |||
}; | |||
([[media:Dot_product.cpp | Źródło: dot_product.cpp]]) | |||
Po raz drugi zwracam uwagę na kolejność parametrów szablonu na liście | Po raz drugi zwracam uwagę na kolejność parametrów szablonu na liście | ||
parametrów. Dzięki temu że niededukowalny parametr | parametrów. Dzięki temu, że niededukowalny parametr <tt>N</tt> jest na | ||
pierwszym miejscu wystarczy podać jawnie tylko jego, drugi parametr | pierwszym miejscu wystarczy podać jawnie tylko jego, drugi parametr | ||
typu | typu <tt>T</tt> zostanie sam automatycznie wydedukowany na podstawie | ||
przekazanych argumentów wywołania funkcji: | przekazanych argumentów wywołania funkcji: | ||
main() { | main() { | ||
double x[3],y[3]; | double x[3],y[3]; | ||
dot_product<3>(x,y); | dot_product<3>(x,y); | ||
} | } | ||
([[media:Dot_product.cpp | Źródło: dot_product.cpp]]) | |||
Parametry pozatypowe są zresztą "ciężko dedukowalne". Właściwie | Parametry pozatypowe są zresztą "ciężko dedukowalne". Właściwie | ||
jedynym sposobem na przekazania wartości stałej poprzez typ argumentu | jedynym sposobem na przekazania wartości stałej poprzez typ argumentu | ||
wywołania jest skorzystanie z parametrów | wywołania jest skorzystanie z parametrów będących szablonami | ||
klas (zob. następny podrozdział). | klas (zob. następny podrozdział). | ||
Używając pozatypowych parametrów szablonów musimy pamiętać że | Używając pozatypowych parametrów szablonów musimy pamiętać, że | ||
odpowiadające im argumenty muszą być stałymi wyrażeniami czasu | odpowiadające im argumenty muszą być stałymi wyrażeniami czasu | ||
kompilacji. Stąd jeżeli używamy typów wskaźnikowych muszą to być | kompilacji. Stąd jeżeli używamy typów wskaźnikowych, muszą to być | ||
wskaźniki do obiektów łączonych zewnętrznie, a nie lokalnych. Ponieważ | wskaźniki do obiektów łączonych zewnętrznie, a nie lokalnych. Ponieważ | ||
jednak jeszcze ani razu nie używałem pozatypowych parametrów szablonów | jednak jeszcze ani razu nie używałem pozatypowych parametrów szablonów | ||
Linia 469: | Linia 452: | ||
===Szablony parametrów szablonu=== | ===Szablony parametrów szablonu=== | ||
Jak już wspomniałem w poprzednim podrozdziale parametrami szablonu | Jak już wspomniałem w poprzednim podrozdziale, parametrami szablonu | ||
funkcji mogą być również szablony klas (zob. następny podrozdział). | funkcji mogą być również szablony klas (zob. następny podrozdział). | ||
Szablony parametrów szablonu umożliwiają | Szablony parametrów szablonu umożliwiają przekazanie nazwy szablonu | ||
jako argumentu szablonu funkcji. Więcej o nich napiszę w drugiej | jako argumentu szablonu funkcji. Więcej o nich napiszę w drugiej | ||
części wykładu. Tutaj jako ciekawostkę | części wykładu. Tutaj tylko pokażę jako ciekawostkę w jaki sposób można | ||
dedukować wartości pozatypowych argumentów szablonu. | dedukować wartości pozatypowych argumentów szablonu. | ||
template< template<int N> class C,int K> | template< template<int N> class C,int K> | ||
<i>taka definicja oznacza, że parametr C określa szablon klasy | |||
szablon klasy | posiadający jeden parametr typu <tt>int</tt>. Parametr N służy tylko | ||
do definicji szablonu C i nie może być użyty nigdzie indziej</i> | |||
użyty nigdzie indziej | void f(C<K>){ | ||
void f(C<K>){ | cout<<K<<endl; | ||
cout<<K<<endl; | };<br> | ||
}; | template<int N> struct SomeClass {};<br> | ||
main() { | |||
template<int N> struct SomeClass {}; | SomeClass<1> c1; | ||
SomeClass<2> c2;<br> | |||
main() { | f(c1); <i>C<nowiki>=</nowiki>SomeClass K<nowiki>=</nowiki>1</i> | ||
SomeClass<1> c1; | f(c2); <i>C<nowiki>=</nowiki>SomeClass K<nowiki>=</nowiki>2</i> | ||
SomeClass<2> c2; | } | ||
([[media:Deduce_N.cpp | Źródło: deduce_N.cpp]]) | |||
===Szablony metod=== | ===Szablony metod=== | ||
Linia 500: | Linia 480: | ||
Jak na razie definiowaliśmy szablony zwykłych funkcji. C++ umożliwia | Jak na razie definiowaliśmy szablony zwykłych funkcji. C++ umożliwia | ||
również definiowanie szablonów metod klasy np.: | również definiowanie szablonów metod klasy np.: | ||
<Source> | |||
struct Max { | struct Max { | ||
template<typename T> max(T a,T b) {return (a>b)?a:b;} | template<typename T> T max(T a,T b) {return (a>b)?a:b;} | ||
}; | }; | ||
main() { | main() { | ||
Max m; | Max m; | ||
m.max(1,2); | m.max(1,2); | ||
} | } | ||
/ | </Source> | ||
([[media:Max_method.cpp | Źródło: max_method.cpp]]) | |||
Szablonów metod składowych dotyczą takie same reguły jak szablonów funkcji. | Szablonów metod składowych dotyczą takie same reguły jak szablonów funkcji. | ||
Linia 518: | Linia 499: | ||
Uwagi na początku poprzedniego rozdziału odnoszą się w tej samej | Uwagi na początku poprzedniego rozdziału odnoszą się w tej samej | ||
mierze do klas jak i do funkcji. I tutaj mamy do czynienia z kodem | mierze do klas, jak i do funkcji. I tutaj mamy do czynienia z kodem, | ||
który w niezmienionej postaci musimy powielać dla różnych typów. | który w niezmienionej postaci musimy powielać dla różnych typów. | ||
Sztandarowym przykładem takiego kodu są różnego rodzaju kontenery(pojemniki) | Sztandarowym przykładem takiego kodu są różnego rodzaju kontenery (pojemniki), | ||
czyli obiekty służące do przechowywania innych obiektów. Jest | czyli obiekty służące do przechowywania innych obiektów. Jest | ||
oczywiste że kod kontenera jest w dużej mierze niezależny od typu | oczywiste, że kod kontenera jest w dużej mierze niezależny od typu | ||
obiektów w nim przechowywanych. Jako przykład weźmy | obiektów w nim przechowywanych. Jako przykład weźmy | ||
sobie stos liczb całkowitych. Możliwa definicja klasy stos może | sobie stos liczb całkowitych. Możliwa definicja klasy stos może | ||
Linia 528: | Linia 509: | ||
w prawdziwych aplikacjach: | w prawdziwych aplikacjach: | ||
<Source> | |||
class Stack { | class Stack { | ||
private: | private: | ||
int rep[N]; | |||
int rep[N]; | size_t top; | ||
public: | public: | ||
static const size_t N | static const size_t N=100; | ||
Stack():_top(0) {}; | |||
void push(int val) {_rep[_top++]=val;} | |||
void push(int val) {_rep[_top++] | int pop() {return rep[--top];} | ||
int pop() | bool is_empty {return (top==0);} | ||
bool is_empty {return (top | |||
} | } | ||
</Source> | |||
Ewidentnie ten kod będzie identyczny dla stosu obiektów dowolnego | Ewidentnie ten kod będzie identyczny dla stosu obiektów dowolnego | ||
innego typu pod warunkiem że typ ten posiada zdefiniowany | innego typu, pod warunkiem, że typ ten posiada zdefiniowany | ||
<tt>operator<nowiki>=</nowiki>()</tt> i konstruktor kopiujący. | |||
W celu zaimplementowania kontenerów bez pomocy szablonów możemy | W celu zaimplementowania kontenerów bez pomocy szablonów możemy | ||
probować podobnych sztuczek jak te opisane w poprzednim rozdziale. W | probować podobnych sztuczek jak te opisane w poprzednim rozdziale. W | ||
językach takich jak Java czy Smalltalk które posiadają uniwersalną | językach takich jak Java czy Smalltalk, które posiadają uniwersalną | ||
klasę | klasę <tt>Object</tt>, z której są dziedziczone wszystkie inne klasy, a nie | ||
posiadają (Java już posiada) szablonów, uniwersalne kontenery są | posiadają (Java już posiada) szablonów, uniwersalne kontenery są | ||
implementowane właśnie poprzez rzutowanie na ten ogólny typ. W | implementowane właśnie poprzez rzutowanie na ten ogólny typ. W | ||
przypadku C++ nawet to rozwiązanie nie jest praktyczne bo C++ nie | przypadku C++ nawet to rozwiązanie nie jest praktyczne, bo C++ nie | ||
posiada pojedynczej | posiada pojedynczej hierarchii klas. | ||
===Szablony klas=== | ===Szablony klas=== | ||
Linia 561: | Linia 539: | ||
Rozwiązaniem są znów szablony, tym razem szablony klas. Podobnie jak w | Rozwiązaniem są znów szablony, tym razem szablony klas. Podobnie jak w | ||
przypadku szablonów funkcji, szablon klasy definiuje nam w | przypadku szablonów funkcji, szablon klasy definiuje nam w | ||
rzeczywistości całą rodzinę klas. Szablon klasy | rzeczywistości całą rodzinę klas. Szablon klasy <tt>Stack</tt> możemy zapisać | ||
następująco: | następująco: | ||
<Source> | |||
template<typename T> class Stack { | template<typename T> class Stack { | ||
public: | public: | ||
static const size_t N | static const size_t N=100; | ||
private: | private: | ||
T _rep[N]; | |||
T | size_t _top;<br> | ||
size_t | |||
public: | public: | ||
Stack():_top(0) {}; | |||
void push(T val) {_rep[_top++]=val;} | |||
T pop() {return _rep[--_top];} | |||
bool is_empty {return (_top==0);} | |||
}; | |||
</Source> | |||
([[media:Stack.cpp | Źródło: stack.cpp]]) | |||
Tak zdefiniowanego szablonu możemy używać podając jawnie jego argumenty. | Tak zdefiniowanego szablonu możemy używać podając jawnie jego argumenty. | ||
<Source> | |||
Stack<string> st ; | Stack<string> st ; | ||
st.push("ania"); | st.push("ania"); | ||
st.push("asia"); | st.push("asia"); | ||
st.push("basia"); | st.push("basia"); | ||
while(!st.is_empty() ){ | while(!st.is_empty() ){ | ||
cout<<st.pop()<<endl; | |||
} | } | ||
/ | </Source> | ||
([[media:Stack.cpp | Źródło: stack.cpp]]) | |||
Dla szablonów klas nie ma możliwości automatycznej dedukcji argumentów | Dla szablonów klas nie ma możliwości automatycznej dedukcji argumentów | ||
szablonu ponieważ klasy nie posiadają argumentów wywołania które | szablonu, ponieważ klasy nie posiadają argumentów wywołania, które | ||
mogłyby do tej dedukcji posłużyć. Jest natomiast możliwość podania | mogłyby do tej dedukcji posłużyć. Jest natomiast możliwość podania | ||
argumentów domyślnych np. | argumentów domyślnych, np. | ||
<Source> | |||
template<typename T | template<typename T = int> Stack { | ||
... | ... | ||
} | } | ||
/ | </Source> | ||
([[media:Stack.cpp | Źródło: stack.cpp]]) | |||
Wtedy możemy korzystać ze stosu bez podawania argumentów szablonu i | Wtedy możemy korzystać ze stosu bez podawania argumentów szablonu i | ||
wyrażenie | wyrażenie | ||
Stack s; | <Source>Stack s;</Source> | ||
będzie równoważne wyrażeniu: | będzie równoważne wyrażeniu: | ||
Stack<int> s; | <Source>Stack<int> s;</Source> | ||
Dla domyślnych argmentów szablonów klas obowiązują te same reguły co dla | Dla domyślnych argmentów szablonów klas obowiązują te same reguły, co dla | ||
domyślnych argumentów wywołania funkcji. | domyślnych argumentów wywołania funkcji. | ||
Należy pamiętać że każda konkretyzacja | Należy pamiętać, że każda konkretyzacja szablonu klasy dla | ||
różniących się zestawów argumentów jest osobną klasą: | różniących się zestawów argumentów jest osobną klasą: | ||
<Source> | |||
Stack<int> si; | Stack<int> si; | ||
Stack<double> sd; | Stack<double> sd; | ||
sd | sd=si; //błąd: to są obiekty różnych klas a nie zdefiniowano przypisania | ||
/ | </Source> | ||
([[media:Stack.cpp | Źródło: stack.cpp]]) | |||
Okazuje się, że próba zdefiniowania operatora przypisania, który | |||
Okazuje się | np. przypisywałby do siebie stosy różnych typów, nie jest łatwa, | ||
np. przypisywałby do siebie stosy różnych typów nie jest łatwa | |||
ponieważ dwa takie stosy nie widzą swoich reprezentacji. | ponieważ dwa takie stosy nie widzą swoich reprezentacji. | ||
Linia 634: | Linia 613: | ||
Zestaw możliwych parametrów szablonów klas jest taki sam jak dla | Zestaw możliwych parametrów szablonów klas jest taki sam jak dla | ||
szablonów funkcji. Podobnie najczęściej wykorzystywane są wyrażenia | szablonów funkcji. Podobnie najczęściej wykorzystywane są wyrażenia | ||
całkowitoliczbowe. W naszym przykładzie ze stosem | całkowitoliczbowe. W naszym przykładzie ze stosem możemy ich użyć do | ||
przekazania rozmiaru stosu: | przekazania rozmiaru stosu: | ||
template<typename T <nowiki>=</nowiki> int , size_t N <nowiki>=</nowiki> 100> class Stack { | template<typename T <nowiki>=</nowiki> int , size_t N <nowiki>=</nowiki> 100> class Stack { | ||
private: | private: | ||
T rep[N]; | T rep[N]; | ||
size_t top; | size_t top; | ||
public: | public: | ||
Stack():_top(0) {}; | Stack():_top(0) {};<br> | ||
void push(T val) {_rep[_top++]<nowiki>=</nowiki>val;} | |||
T pop() {return rep[--top];} | |||
bool is_empty {return (top<nowiki>=</nowiki><nowiki>=</nowiki>0);} | |||
} | |||
([[media:Stack_N.cpp | Źródło: stack_N.cpp]]) | |||
Podkreślam jeszcze raz że | Podkreślam jeszcze raz, że <tt>Stack<int,100></tt> i <tt>Stack<int,101></tt> to | ||
dwie różne klasy. | dwie różne klasy. | ||
===Szablony parametrów szablonu=== | ===Szablony parametrów szablonu=== | ||
Stos jest nie tyle strukturą danych ile sposobem dostępu do nich. | Stos jest nie tyle strukturą danych, ile sposobem dostępu do nich. | ||
Stos realizuje regułę LIFO czyli Last In First Out. W tym sensie nie | Stos realizuje regułę LIFO czyli Last In First Out. W tym sensie nie | ||
jest istotne w jaki sposób dane są na stosie przechowywane. Może to | jest istotne w jaki sposób dane są na stosie przechowywane. Może to | ||
być tablica jak w powyższych przykładach, ale może to być praktycznie | być tablica, jak w powyższych przykładach, ale może to być praktycznie | ||
dowolny inny kontener. Np. w Standardowej Bibliotece Szablonów C++ | dowolny inny kontener. Np. w Standardowej Bibliotece Szablonów C++ | ||
stos jest zaimplementowany jako | |||
adapter do któregoś z istniejących już kontenerów. Ponieważ kontenery | adapter do któregoś z istniejących już kontenerów. Ponieważ kontenery | ||
STL są szablonami, szablon adaptera mógłby wyglądać następująco: | STL są szablonami, szablon adaptera mógłby wyglądać następująco: | ||
<Source> | |||
template<typename T, | template<typename T, | ||
template<typename X > class Sequence | template<typename X > class Sequence=std::deque > | ||
class Stack { | class Stack { | ||
Sequence<T> _rep; | Sequence<T> _rep; | ||
public: | public: | ||
void push(T e) {_rep.push_back(e);}; | void push(T e) {_rep.push_back(e);}; | ||
T pop() {T top | T pop() {T top=_rep.top();_rep.pop_back();return top;} | ||
bool is_empty() const {return _rep.empty();} | bool is_empty() const {return _rep.empty();} | ||
}; | }; | ||
</Source> | |||
Konkretyzując stos możemy wybrać kontener w którym będą przechowywane | Konkretyzując stos możemy wybrać kontener, w którym będą przechowywane | ||
jego elementy: | jego elementy: | ||
<Source> | |||
Stack<double,std::vector> sv; | Stack<double,std::vector> sv; | ||
</Source> | |||
Można | Można zamiast szablonu użyć zwykłego parametru typu: | ||
<Source> | |||
template<typename T,typename C > class stos { | template<typename T,typename C > class stos { | ||
C rep; | C rep; | ||
public: | public: | ||
... | ... | ||
} | } | ||
/ | </Source> | ||
([[media:Stack_adapter.cpp | Źródło: stack_adapter.cpp]]) | |||
i używać go w następujący sposób: | i używać go w następujący sposób: | ||
<Source> | |||
stos<double,std::vector<double> > sv; | stos<double,std::vector<double> > sv; | ||
</Source> | |||
W przypadku użycia szablonu jako parametru szablonu | W przypadku użycia szablonu jako parametru szablonu | ||
zapewniamy konsystencję pomiędzy typem <tt>T</tt> i kontenerem <tt>C</tt>, | |||
uniemożliwiając pomyłkę podstawienia | |||
niepasujących parametrów: | niepasujących parametrów: | ||
stos<double,std::vector<int> > sv; | <Source> | ||
stos<double,std::vector<int> > sv; <i>błąd: niezgodność typow</i> | |||
Uczciwość nakazuje jednak w tym miejscu stwierdzić że właśnie takie | </Source> | ||
rozwiązanie jest zastosowane w STL-u. Ma ono | Uczciwość nakazuje jednak w tym miejscu stwierdzić, że właśnie takie | ||
adaptować na stos dowolny kontener niekoniecznie będący szablonem. | rozwiązanie jest zastosowane w STL-u. Ma ono tę zaletę, że możemy | ||
adaptować na stos dowolny kontener, niekoniecznie będący szablonem. | |||
Na koniec jeszcze jedna uwaga: szablony kontenerów z STL posiadają po | Na koniec jeszcze jedna uwaga: szablony kontenerów z STL posiadają po | ||
dwa parametry typów z tym że drugi posiada wartość domyślną | dwa parametry typów, z tym, że drugi posiada wartość domyślną (standard dopuszcza dowolną ilość argumentów w implemetacji kontenerów STL jak długo będą one posiadały wartości domyślne). Autorzy D. Vandervoorde, N. Josuttis <i>"C++ Szablony, Vademecum profesjonalisty"</i> ostrzegają, że w tej sytuacji kompilator może nie zaakceptować wyrażenia: | ||
kontenerów STL jak długo | |||
Autorzy | |||
nie | |||
<Source> | |||
stos<double,std::vector> sv; | stos<double,std::vector> sv; | ||
</Source> | |||
ponieważ ignoruje fakt istnienia wartości domyślnej dla | ponieważ ignoruje fakt istnienia wartości domyślnej dla | ||
drugiego parametru | drugiego parametru szablonu <tt>std::vector</tt>. | ||
Mamy wtedy niezgodność pomiędzy przekazanym argumentem szablonu | Mamy wtedy niezgodność pomiędzy przekazanym argumentem szablonu | ||
<Source> | |||
template<typename T> | template<typename T> | ||
std::vector<T,typename A | std::vector<T,typename A = std::allocator<T> >; | ||
</Source> | |||
oraz deklaracją paremetru | oraz deklaracją paremetru <tt>Sequence</tt> jako: | ||
<Source> | |||
template<typename X > class Sequence ; | template<typename X > class Sequence ; | ||
</Source> | |||
która zakłada tylko jeden parametr szablonu. Można wtedy zmienić | która zakłada tylko jeden parametr szablonu. Można wtedy zmienić | ||
deklarację szablonu stos i podać domyślny argument dla szablony w | deklarację szablonu <tt>stos</tt> i podać domyślny argument dla szablony w | ||
liście parametrów: | |||
template<typename T,template< | <Source> | ||
template<typename T,template<typename X ,typename A = | |||
std::allocator<X> > class C > class stos {...} | std::allocator<X> > class C > class stos {...} | ||
</Source> | |||
W praktyce używane przeze mnie kompilatory (g++ wersja ><nowiki>=</nowiki> 3.3) nie | W praktyce używane przeze mnie kompilatory (g++ wersja ><nowiki>=</nowiki> 3.3) nie | ||
wymagały takiej konstrukcji. Przyznaję że | wymagały takiej konstrukcji. Przyznaję, że nie udało mi się doczytać czy | ||
jest to cecha kompilatora g++ czy nowego standardu C++(autorzy | jest to cecha kompilatora g++, czy nowego standardu C++ (autorzy D. Vandervoorde, N. Josuttis <i>"C++ Szablony, Vademecum profesjonalisty"</i> opierali się na poprzednim wydaniu standardu). | ||
===Konkretyzacja na żądanie=== | ===Konkretyzacja na żądanie=== | ||
Jak już wspomniałem wcześniej konkretyzacja szablonów może odbywać się | Jak już wspomniałem wcześniej, konkretyzacja szablonów może odbywać się | ||
"na żądanie". W takim przypadku kompilator będzie konkretyzował | "na żądanie". W takim przypadku kompilator będzie konkretyzował | ||
tylko funkcje napotkane w kodzie. I tak jeśli np. nie użyjemy w naszym | tylko funkcje napotkane w kodzie. I tak, jeśli np. nie użyjemy w naszym | ||
kodzie funckji | kodzie funckji <tt>Stack<int>::pop()</tt>, to nie zostanie ona | ||
wygenerowana. Można z tego skorzystać i konkretyzować klasy typami | wygenerowana. Można z tego skorzystać i konkretyzować klasy typami, | ||
które nie spełniają wszystkich ograniczeń nałożonych na parametry | które nie spełniają wszystkich ograniczeń nałożonych na parametry | ||
szablonu. Wszystko bedzię w porządku jak długo nie będziemy używać | szablonu. Wszystko bedzię w porządku jak długo nie będziemy używać | ||
funkcji łamiących te ograniczenia. Np. załóżmy że do szablonu | funkcji łamiących te ograniczenia. Np. załóżmy, że do szablonu | ||
<tt>Stack</tt> dodajemy możliwość jego sortowania (wiem, to nie jest zgodne z duchem programowania obiektowego, stos nie posiada operacji sortowania, puryści mogą zastąpić ten przykład kontenerem <tt>list</tt>): | |||
jest zgodne z duchem programowania obiektowego, stos nie posiada | |||
operacji sortowania, puryści zastąpić ten przykład kontenerem | |||
template<typename T,int N> void Stack<T,N>::sort() { | template<typename T,int N> void Stack<T,N>::sort() { | ||
bubble_sort(_rep,N); | bubble_sort(_rep,N); | ||
}; | }; | ||
Możemy teraz np. używać | Możemy teraz np. używać | ||
Stack<std::complex<double> > sc; | Stack<std::complex<double> > sc; | ||
sc.push( std::complex<double>(0,1)); | sc.push( std::complex<double>(0,1)); | ||
sc.pop(); | sc.pop(); | ||
ale nie | ale nie | ||
sc.sort(); | sc.sort(); | ||
([[media:Stack_sort.cpp | Źródło: stack_sort.cpp]]) | |||
Natomiast konkretyzacja jawna | Natomiast konkretyzacja jawna | ||
template Stack<std::complex<double> >; | template Stack<std::complex<double> >; | ||
([[media:Stack_sort.cpp | Źródło: stack_sort.cpp]]) | |||
nie powiedzie się, bo kompilator będzie się starał skonkretyzować | nie powiedzie się, bo kompilator będzie się starał skonkretyzować | ||
wszystkie składowe klasy | wszystkie składowe klasy <tt>Stack</tt>, w tym metodę <tt>sort()</tt>. | ||
===Typy stowarzyszone=== | ===Typy stowarzyszone=== | ||
Linia 779: | Linia 768: | ||
przydatne w przypadku szablonów. Rozważmy następujący przykład: | przydatne w przypadku szablonów. Rozważmy następujący przykład: | ||
template<typename T> Stack { | template<typename T> Stack { | ||
public: | public: | ||
typedef T value_type; | typedef T value_type; | ||
... | ... | ||
} | } | ||
Możemy teraz używać tej definicji w innych szablonach | Możemy teraz używać tej definicji w innych szablonach | ||
template<typename S> void f(S s) { | template<typename S> void f(S s) { | ||
typename S::value_type total; | typename S::value_type total; | ||
<i>słowo <tt>typename</tt> jest wymagane, inaczej kompilator założy, że | |||
kompilator założy że <tt>S:: | <tt>S::value_type</tt> odnosi się do statycznej składowej klasy</i> | ||
składowej klasy | while(!s.is_empty() ) { | ||
while(!s.is_empty() ) { | total+<nowiki>=</nowiki>s.pop(); | ||
total+<nowiki>=</nowiki>s.pop(); | } | ||
} | return total; | ||
return total; | } | ||
} | |||
([[media:Stack_N.cpp | Źródło: stack_N.cpp]]) | |||
Bez takich możliwości musielibyśmy przekazać typ elementów stosu w | Bez takich możliwości musielibyśmy przekazać typ elementów stosu w | ||
osobnym argumencie. Mechanizm typów stowarzyszonych jest | osobnym argumencie. Mechanizm typów stowarzyszonych jest | ||
bardzo czesto używany w uogólnionym kodzie. | bardzo czesto używany w uogólnionym kodzie. | ||
Aktualna wersja na dzień 12:10, 19 paź 2021
Szablony funkcji
Funkcje uogólnione
W praktyce programowania często spotykamy się z funkcjami (algorytmami), które można zastosować do szerokiej klasy typów i struktur danych. Typowym przykładem jest funkcja obliczająca maksimum dwu wartości. Ten trywialny, aczkolwiek przydatny kod można zapisać np. w postaci:
int max(int a,int b) { return (a>b)?a:b; };
Przykład 1.1
Funkcja max wybiera większy z dwu int-ów, ale widać, że kod będzie identyczny dla argumentów dowolnego innego typu pod warunkiem, iż istnieje dla niego operator porównania i konstruktor kopiujący. W językach programowania z silną kontrolą typów, takich jak C, C++ czy Java definiując funkcję musimy jednak podać typy przekazywanych parametrów oraz typ wartości zwracanej. Oznacza to, że dla każdego typu argumentów musimy definiować nową funkcję max:
int max(int a, int b) {return (a>b)?a:b;}; double max(double a,double b) {return (a>b)?a:b;}; string max(string a,string b) {return (a>b)?a:b;}; skorzystaliśmy tu z dostępnej w C++ możliwości przeładowywania funkcji
main() { cout<< max(7,5)<<end; cout<< max(3.1415,2.71)<<endl; cout<< max("Ania","Basia")<<endl; }
Przykład 1.2
Takie powtarzanie kodu, poza oczywistym zwiększeniem nakładu pracy, ma inne niepożądane efekty, związane z trudnością zapewnienia synchronizacji kodu każdej z funkcji. Jeśli np. zauważymy błąd w kodzie, to musimy go poprawić w kilku miejscach. To samo dotyczy optymalizacji kodu. W powyższym przykładzie kod jest wyjątkowo prosty, ale taki sam problem dotyczy np. funkcji sortujących. Rozważmy prosty algorytm sortowania bąbelkowego:
inline void swap(double &a,double &b) { double tmp=a;a=b;b=tmp; } void buble_sort(double *data,int N) { for(int i=n-1;i>0;--i) for(int j=0; j < i;++j) if(data[j]>data[j+1]) swap(data[j],data[j+1]); }
Powyższa funkcja sortuje tablicę zawierającą wartości typu double, ale widać, że znów kod będzie identyczny, jeśli zamiast double użyjemy dowolnego innego typu, którego wartości możemy porównywać za pomocą funkcji operator>() i dla którego zdefiniowany jest operator przypisania. Co więcej, kod nie zmieni się jeśli zamiast tablicy użyjemy dowolnej innej struktury danych, umożliwiającej indeksowany dostęp do swoich składowych, np. std::vector ze Standardowej Biblioteki Szablonów STL. W tym przypadku kod jest już bardziej skomplikowany i kłopoty związane z jego powielaniem będą większe. Przykłady takie można mnożyć, istnieje bowiem wiele takich funkcji czy algorytmów uogólnionych. Ich kod może być znacznie bardziej skomplikowany niż w podanych przykładach, a zależność od typu argumentów nie musi ograniczać się do sygnatury, ale występować również we wnętrzu funkcji, jak np. w przypadku zmiennej tmp w funkcji swap. Powielanie takiego kodu dla różnych typów parametrów może łatwo prowadzić do błędów, utrudnia ich wykrywanie, a konieczność edycji każdego egzemplarza kodu zniechęca do wprowadzania ulepszeń.
Funkcje uogólnione bez szablonów
Jak radzili, a właściwie jak radzą sobie programiści bez możliwości skorzystania z szablonów? Tradycyjne sposoby rozwiązywania tego typu problemów to między innymi makra:
#define max(a,b) ( (a>b)?a:b) )
lub używanie wskaźników typów ogólnych, takich jak void *, jak np. w funkcji qsort ze standardowej biblioteki C:
void qsort(void *base, size_t nmemb, size_t size,
int(*compar)(const void *, const void *));
Mimo iż użyteczne, żadne z tych rozwiązań nie może zostać uznane za wystarczająco ogólne i bezpieczne.
Można się również pokusić o próbę rozwiązania tego problemu za pomocą mechanizmów programowania obiektowego. W sumie jest to bardziej wyrafinowana odmiana rzutowania na void *. Polega na zdefiniowaniu ogólnego typu dla obiektów, które mogą być porównywane:
class GreaterThanComparable {
public:
virtual bool operator>(const GreaterThanComparable &) const = 0;
};
następnie zdefiniowaniu funkcji max w postaci:
const GreaterThanComparable &
max(const GreaterThanComparable &a,
const GreaterThanComparable &b) {
return (a>b)? a:b;
}
i używaniu jej np. w następujący sposób:
class Int:public GreaterThanComparable {
int val;
public: Int(int i <nowiki>=</nowiki> 0):val(i) {};
operator int() {return val;};
virtual bool
operator>(const GreaterThanComparable &b) const {
return val > static_cast<const Int&>(b).val;
}
};
main() {
Int a(1),b(2);
Int c;
c = (const Int &)::max(a,b);
cout<<(int)c<<endl;
}
Widać więc wyraźnie, że to podejście wymaga sporego nakładu pracy, a więc w szczególności w przypadku tak prostej funkcji jak max jest wysoce niepraktyczne. Ogólnie rzecz biorąc ma ono następujące wady:
- Wymaga dziedziczenia z abstrakcyjnej klasy bazowej GreaterThanComparable, czyli może być zastosowane tylko do typów zdefiniowanych przez nas. Inne typy, w tym typy wbudowane, wymagają kopertowania w klasie opakowującej, takiej jak klasa Int w powyższym przykładzie.
- Ponieważ potrzebujemy polimorfizmu funkcja operator>() musi być funkcją wirtualną, a więc musi być funkcją składową klasy i nie może być typu inline. W przypadku tak prostych funkcji niemożność rozwinięcia ich w miejscu wywołania może prowadzić do dużych narzutów w czasie wykonania.
- Funkcja max zwraca zawsze referencje do GreaterThanComparable, więc konieczne jest rzutowanie na typ wynikowy (tu Int).
Szablony funkcji
Widać, że podejście obiektowe nie nadaje się najlepiej do rozwiązywania tego szczególnego problemu powielania kodu. Dlatego w C++ wprowadzono nowy mechanizm: szablony. Szablony zezwalają na definiowanie całych rodzin funkcji, które następnie mogą być używane dla różnych typów argumentów.
Definicja szablonu funkcji max, odpowiadającej definicji 1.1 wygląda następująco:
template<typename T> T max(T a,T b) {return (a>b)?a:b;};
Przyjrzyjmy się jej z bliska. Wyrażenie template<typename T> oznacza, że mamy do czynienia z szablonem, który posiada jeden parametr formalny nazwany T. Słowo kluczowe typename oznacza, że parametr ten jest typem (nazwą typu). Zamiast słowa typename możmy użyć słowa kluczowego class. Nazwa tego parametru może być następnie wykorzystywana w definicji funkcji w miejscach, gdzie spodziewamy się nazwy typu. I tak powyższe wyrażenie definiuje funkcję max, która przyjmuje dwa argumenty typu T i zwraca wartość typu T, będącą wartością większego z dwu argumentów. Typ T jest na razie niewyspecyfikowany. W tym sensie szablon definiuje całą rodzinę funkcji. Konkretną funkcję z tej rodziny wybieramy poprzez podstawienie za formalny parametr T konkretnego typu będącego argumentem szablonu. Takie podstawienie nazywamy konkretyzacją szablonu. Argument szablonu umieszczamy w nawiasach ostrych za nazwą szablonu (w praktyce można uniknąć konieczności jawnej specyfikacji argumentów szablonu, opiszemy to w następnych częściach wykładu):
int i,j,k;
k=max<int>(i,j);
Takie użycie szablonu spowoduje wygenerowanie identycznej funkcji jak 1.1. W powyższym przypadku za T podstawiamy int. Oczywiście możemy podstawić za T dowolny typ i używając szablonów program 1.2 można zapisać następująco:
template<typename T> T max(T a,T b) {return (a>b)?a:b;}
main() {
cout<<::max<int>(7,5)<<endl;
cout<<::max<double>(3.1415,2.71)<<endl;
cout<<::max<string>("Ania","Basia")<<endl;
}
W powyższym kodzie użyliśmy konstrukcji ::max(a,b). Dwa dwukropki oznaczają, że używamy funkcji max zdefiniowanej w ogólnej przestrzeni nazw. Jest to konieczne aby kod się skompilował, ponieważ szablon max istnieje już w standardowej przestrzeni nazw std. W dalszej części wykładu będziemy te podwójne dwukropki pomijać.
Oczywiście istnieją typy których podstawienie spowoduje błędy kompilacji, np.
complex<double> c1,c2;
max<complex<double> >(c1,c2); //brak operatora >
lub
class X {
private:
X(const X &){};
};
X a,b;
max<X>(a,b); //prywatny (niewidoczny) konstruktor kopiujący
Ogólnie rzecz biorąc, każdy szablon definiuje pewną klasę typów, które mogą zostać podstawione jako jego argumenty.
Dedukcja argumentów szablonu
Użyteczność szablonów funkcji zwiększa istotnie fakt, że argumenty szablonu nie muszą być podawane jawnie. Kompilator może je wydedukować z argumentów funkcji. Tak więc zamiast
int i,j,k; k=max<int>(i,j);
możemy napisać
int i,j,k; k=max(i,j);
i kompilator zauważy, że tylko podstawienie int-a za T umożliwi dopasowanie sygnatury funkcji do parametrów jej wywołania i automatycznie dokona odpowiedniej konkretyzacji.
Może się zdarzyć, że podamy takie argumenty funkcji, że dopasowanie argumentów wzorca będzie niemożliwe, otrzymamy wtedy błąd kompilacji. Trzeba pamiętać, że mechanizm automatycznego dopasowywania argumentów szablonu powoduje wyłączenie automatycznej konwersji argumentów funkcji. Podanie jawnie argumentów szablonu (w nawiasach ostrych za nazwą szablonu) jednoznacznie określa sygnaturę funkcji, a więc umożliwia automatyczną konwersję typów. Ilustruje to poniższy kod:
template<typename T> T max(T a,T b) {return (a>b)?a:b;}
main() {
cout<<::max(3.14,2)<<endl;
// błąd: kompilator nie jest w stanie wydedukowac argumentu szablonu, bo typy
// argumentów (double,int) nie pasują do (T,T)
cout<<::max<int>(3.14,2)<<endl;
// podając argument jawnie wymuszamy sygnaturę int max(int,int), a co za tym
// idzie automatyczną konwersję argumentu 1 do int-a
cout<<::max<double>(3.14,2)<<endl;
// podając argument szablonu jawnie wymuszamy sygnaturę
// double max(double,double)
// a co za tym idzie automatyczną konwersję argumentu 2 do double-a
int i;
cout<<::max<int *>(&i,i)<<endl;
//błąd: nie istnieje konwersja z typu int na int*
Może warto zauważyć, że automatyczna dedukcja parametrów szablonu jest możliwa tylko wtedy, jeśli parametry wywołania funkcji w jakiś sposób zależą od parametrów szablonu. Jeśli tej zależności nie ma, z przyczyn oczywistych dedukcja nie jest możliwa i trzeba parametry podawać jawnie. Wtedy istotna jest kolejność parametrów na liście. Jeżeli parametry, których nie da się wydedukować, umieścimy jako pierwsze, wystarczy, że tylko je podamy jawnie, a kompilator wydedukuje resztę. Ilustruje to poniższy kod:
template<typename T,typename U> T convert(U u) { return (T)u; }; template<typename U,typename T> T inv_convert(U u) { return (T)u; }; fukcje różnią się tylko kolejnością parametrów szablonu
main() { cout<<convert(33)<<endl; błąd: kompilator nie jest w stanie wydedukować pierwszego parametru szablonu, bo nie zależy on od parametru wywołania funkcji
cout<<convert<char>(33)<<endl; w porządku: podajemy jawnie argument T, kompilator sam dedukuje argument U z typu argumentu wywołania funkcji
cout<<inv_convert<char>('a')<<endl; błąd: podajemy jawnie argument odpowiadający parametrowi U. Kompilator nie jest w stanie wydedukować argumentu T, bo nie zależy on od argumentu wywołania funkcji
cout<<inv_convert<int,char>(33)<<endl; w porządku: podajemy jawnie oba argumenty szablonu }
Używanie szablonów
Z użyciem szablonów wiąże się parę zagadnień niewidocznych w prostych przykładach. W językach C i C++ zwykle rozdzielamy deklarację funkcji od jej definicji i zwyczajowo umieszczamy deklarację w plikach nagłówkowych *.h, a definicję w plikach źródłowych *.c, *.cpp itp. Pliki nagłówkowe są w czasie kompilacji włączane do plików, w których chcemy korzystać z danej funkcji, a pliki źródłowe są pojedynczo kompilowane do plików “obiektowych” *.o. Następnie pliki obiektowe są łączone w jeden plik wynikowy (zob. rysunek 1.1). W pliku korzystającym z danej funkcji nie musimy więc znać jej definicji, a tylko deklarację. Na podstawie nazwy funkcji konsolidator powiąże wywołanie funkcji z jej implementacją znajdującą się w innym pliku obiektowym. W ten sposób tylko zmiana deklaracji funkcji wymaga rekompilacji plików, w których z niej korzystamy, a zmiana definicji wymaga jedynie rekompilacji pliku, w którym dana funkcja jest zdefiniowana.
Taka organizacja umożliwia przestrzeganie "reguły jednej definicji" (one definition rule), wymaganej przez C++. Osobom nieobeznanym z programowaniem w C/C++ zwracam uwagę na konstrukcję
#ifndef _nazwa_pliku_ #define _nazwa_pliku_ ... #endif
uniemożliwiajacą podwójne włączenie tego pliku do jednej jednostki translacyjnej.
Podobne podejście do kompilacje szablonów się nie powiedzie (zob. rysunek 1.2). Powodem jest fakt, że w trakcie kompilacji pliku utils.cpp kompilator nie wie jeszcze, że potrzebna będzie funkcja max<int>, wobec czego nie generuje kodu żadnej funkcji, a jedynie sprawdza poprawność gramatyczną szablonu. Z kolei podczas kompilacji pliku main.cpp kompilator już wie, że ma skonkretyzować szablon dla T = int, ale nie ma dostępu do kodu szablonu.

Istnieją różne rozwiązania tego problemu. Najprościej chyba jest zauważyć, że opisane zachowanie jest analogiczne do zachowania podczas kompilacji funkcji rozwijanych w miejscu wywołania (inline), których definicja również musi być dostępna w czasie kompilacji. Podobnie więc jak w tym przypadku możemy zamieścić wszystkie deklaracje i definicje szablonów w pliknu nagłówkowym, włączanym do plików, w ktorych z tych szablonów korzystamy (zob. rysunek 1.3). Podobnie jak w przypadku funkcji inline reguła jednej definicji zezwala na powtarzanie definicji/deklaracji szablonów w różnych jednostkach translacyjnych, pod warunkiem, że są one identyczne. Stąd konieczność umieszczania ich w plikach nagłówkowych.

Ten sposób organizacji pracy z szablonami, nazywany modelem włączenia, jest najbardziej uniwersalny. Jego główną wadą jestkonieczność rekompilacji całego kodu korzystającego z szablonów przy każdej zmianie definicji szablonu. Również jeśli zmienimy coś w pliku, w którym korzystamy z szablonu, to musimy rekompilować cały kod szablonu włączony do tego pliku, nawet jeśli nie uległ on zmianie. Jeśli się uwzględni fakt, że kompilacja szablonu jest bardziej skomplikowana od kompilacji "zwykłego" kodu, to duży projekt intensywnie korzystający z szablonów może wymagać bardzo długich czasów kompilacji.
Możemy też w jakiś sposób dać znać kompilatorowi, że podczas kompilacji pliku utils.cpp powinien wygenerować kod dla funkcji max<int>. Można to zrobić dodając jawne żądanie konkretyzacji szablonu (zob. rysunek 1.4):
template<typename T> T max(T a,T b) {return (a>b)?a:b;} template int max<int>(int ,int) ; konkretyzacja jawna
Używając konkretyzacji jawnej musimy pamiętać o dokonaniu konkretyzacji każdej używanej funkcji, tak że to podejście nie skaluje się zbyt dobrze. Ponadto w przypadku szablonów klas (omawianych w następnym module) konkretyzacja jawna pociąga za sobą konkretyzację wszystkich metod danej klasy, a konkretyzacja “na żądanie” - jedynie tych używanych w programie.
Pozatypowe parametry szablonów
Poza parametrami określającymi typ, takimi jak parametr T w dotychczasowych przykładach, szablony funkcji mogą przyjmować również parametry innego rodzaju. Obecnie mogą to być inne szablony, co omówię w następnym podrozdziale lub parametry określające nie typ, ale wartości. Jak na razie (w obecnym standardzie) te wartości nie mogą być dowolne, ale muszą mieć jeden z poniższych typów:
- typ całkowitoliczbowy bądź typ wyliczeniowy
- typ wskaźnikowy
- typ referencyjny.
Takie parametry określające wartość nazywamy parametrami pozatypowymi. W praktyce z parametrów pozatypowych najczęściej używa się parametrów typu całkowitoliczbowego. Np.
template<size_t N,typename T> T dot_product(T *a,T *b) { T total=0.0; for(size_t i=0;i<N;++i) total += a[i]*b[i] ;
return total; };
Po raz drugi zwracam uwagę na kolejność parametrów szablonu na liście parametrów. Dzięki temu, że niededukowalny parametr N jest na pierwszym miejscu wystarczy podać jawnie tylko jego, drugi parametr typu T zostanie sam automatycznie wydedukowany na podstawie przekazanych argumentów wywołania funkcji:
main() { double x[3],y[3]; dot_product<3>(x,y); }
Parametry pozatypowe są zresztą "ciężko dedukowalne". Właściwie jedynym sposobem na przekazania wartości stałej poprzez typ argumentu wywołania jest skorzystanie z parametrów będących szablonami klas (zob. następny podrozdział).
Używając pozatypowych parametrów szablonów musimy pamiętać, że odpowiadające im argumenty muszą być stałymi wyrażeniami czasu kompilacji. Stąd jeżeli używamy typów wskaźnikowych, muszą to być wskaźniki do obiektów łączonych zewnętrznie, a nie lokalnych. Ponieważ jednak jeszcze ani razu nie używałem pozatypowych parametrów szablonów innych niż typy całkowite, to nie będę podawał żadnych przykładów takich paremtrów na tym wykładzie.
Szablony parametrów szablonu
Jak już wspomniałem w poprzednim podrozdziale, parametrami szablonu funkcji mogą być również szablony klas (zob. następny podrozdział). Szablony parametrów szablonu umożliwiają przekazanie nazwy szablonu jako argumentu szablonu funkcji. Więcej o nich napiszę w drugiej części wykładu. Tutaj tylko pokażę jako ciekawostkę w jaki sposób można dedukować wartości pozatypowych argumentów szablonu.
template< template<int N> class C,int K> taka definicja oznacza, że parametr C określa szablon klasy posiadający jeden parametr typu int. Parametr N służy tylko do definicji szablonu C i nie może być użyty nigdzie indziej void f(C<K>){ cout<<K<<endl; };
template<int N> struct SomeClass {};
main() { SomeClass<1> c1; SomeClass<2> c2;
f(c1); C=SomeClass K=1 f(c2); C=SomeClass K=2 }
Szablony metod
Jak na razie definiowaliśmy szablony zwykłych funkcji. C++ umożliwia również definiowanie szablonów metod klasy np.:
struct Max {
template<typename T> T max(T a,T b) {return (a>b)?a:b;}
};
main() {
Max m;
m.max(1,2);
}
Szablonów metod składowych dotyczą takie same reguły jak szablonów funkcji.
Szablony klas
Typy uogólnione
Uwagi na początku poprzedniego rozdziału odnoszą się w tej samej mierze do klas, jak i do funkcji. I tutaj mamy do czynienia z kodem, który w niezmienionej postaci musimy powielać dla różnych typów. Sztandarowym przykładem takiego kodu są różnego rodzaju kontenery (pojemniki), czyli obiekty służące do przechowywania innych obiektów. Jest oczywiste, że kod kontenera jest w dużej mierze niezależny od typu obiektów w nim przechowywanych. Jako przykład weźmy sobie stos liczb całkowitych. Możliwa definicja klasy stos może wyglądać następująco, choć nie polecam jej jako wzoru do naśladowania w prawdziwych aplikacjach:
class Stack {
private:
int rep[N];
size_t top;
public:
static const size_t N=100;
Stack():_top(0) {};
void push(int val) {_rep[_top++]=val;}
int pop() {return rep[--top];}
bool is_empty {return (top==0);}
}
Ewidentnie ten kod będzie identyczny dla stosu obiektów dowolnego innego typu, pod warunkiem, że typ ten posiada zdefiniowany operator=() i konstruktor kopiujący.
W celu zaimplementowania kontenerów bez pomocy szablonów możemy probować podobnych sztuczek jak te opisane w poprzednim rozdziale. W językach takich jak Java czy Smalltalk, które posiadają uniwersalną klasę Object, z której są dziedziczone wszystkie inne klasy, a nie posiadają (Java już posiada) szablonów, uniwersalne kontenery są implementowane właśnie poprzez rzutowanie na ten ogólny typ. W przypadku C++ nawet to rozwiązanie nie jest praktyczne, bo C++ nie posiada pojedynczej hierarchii klas.
Szablony klas
Rozwiązaniem są znów szablony, tym razem szablony klas. Podobnie jak w przypadku szablonów funkcji, szablon klasy definiuje nam w rzeczywistości całą rodzinę klas. Szablon klasy Stack możemy zapisać następująco:
template<typename T> class Stack {
public:
static const size_t N=100;
private:
T _rep[N];
size_t _top;<br>
public:
Stack():_top(0) {};
void push(T val) {_rep[_top++]=val;}
T pop() {return _rep[--_top];}
bool is_empty {return (_top==0);}
};
Tak zdefiniowanego szablonu możemy używać podając jawnie jego argumenty.
Stack<string> st ;
st.push("ania");
st.push("asia");
st.push("basia");
while(!st.is_empty() ){
cout<<st.pop()<<endl;
}
Dla szablonów klas nie ma możliwości automatycznej dedukcji argumentów szablonu, ponieważ klasy nie posiadają argumentów wywołania, które mogłyby do tej dedukcji posłużyć. Jest natomiast możliwość podania argumentów domyślnych, np.
template<typename T = int> Stack {
...
}
Wtedy możemy korzystać ze stosu bez podawania argumentów szablonu i wyrażenie
Stack s;
będzie równoważne wyrażeniu:
Stack<int> s;
Dla domyślnych argmentów szablonów klas obowiązują te same reguły, co dla domyślnych argumentów wywołania funkcji.
Należy pamiętać, że każda konkretyzacja szablonu klasy dla różniących się zestawów argumentów jest osobną klasą:
Stack<int> si;
Stack<double> sd;
sd=si; //błąd: to są obiekty różnych klas a nie zdefiniowano przypisania
Okazuje się, że próba zdefiniowania operatora przypisania, który np. przypisywałby do siebie stosy różnych typów, nie jest łatwa, ponieważ dwa takie stosy nie widzą swoich reprezentacji.
Pozatypowe parametry szablonów klas
Zestaw możliwych parametrów szablonów klas jest taki sam jak dla szablonów funkcji. Podobnie najczęściej wykorzystywane są wyrażenia całkowitoliczbowe. W naszym przykładzie ze stosem możemy ich użyć do przekazania rozmiaru stosu:
template<typename T = int , size_t N = 100> class Stack { private: T rep[N]; size_t top; public: Stack():_top(0) {};
void push(T val) {_rep[_top++]=val;} T pop() {return rep[--top];} bool is_empty {return (top==0);} }
Podkreślam jeszcze raz, że Stack<int,100> i Stack<int,101> to dwie różne klasy.
Szablony parametrów szablonu
Stos jest nie tyle strukturą danych, ile sposobem dostępu do nich. Stos realizuje regułę LIFO czyli Last In First Out. W tym sensie nie jest istotne w jaki sposób dane są na stosie przechowywane. Może to być tablica, jak w powyższych przykładach, ale może to być praktycznie dowolny inny kontener. Np. w Standardowej Bibliotece Szablonów C++ stos jest zaimplementowany jako adapter do któregoś z istniejących już kontenerów. Ponieważ kontenery STL są szablonami, szablon adaptera mógłby wyglądać następująco:
template<typename T,
template<typename X > class Sequence=std::deque >
class Stack {
Sequence<T> _rep;
public:
void push(T e) {_rep.push_back(e);};
T pop() {T top=_rep.top();_rep.pop_back();return top;}
bool is_empty() const {return _rep.empty();}
};
Konkretyzując stos możemy wybrać kontener, w którym będą przechowywane jego elementy:
Stack<double,std::vector> sv;
Można zamiast szablonu użyć zwykłego parametru typu:
template<typename T,typename C > class stos {
C rep;
public:
...
}
i używać go w następujący sposób:
stos<double,std::vector<double> > sv;
W przypadku użycia szablonu jako parametru szablonu zapewniamy konsystencję pomiędzy typem T i kontenerem C, uniemożliwiając pomyłkę podstawienia niepasujących parametrów:
stos<double,std::vector<int> > sv; <i>błąd: niezgodność typow</i>
Uczciwość nakazuje jednak w tym miejscu stwierdzić, że właśnie takie rozwiązanie jest zastosowane w STL-u. Ma ono tę zaletę, że możemy adaptować na stos dowolny kontener, niekoniecznie będący szablonem.
Na koniec jeszcze jedna uwaga: szablony kontenerów z STL posiadają po dwa parametry typów, z tym, że drugi posiada wartość domyślną (standard dopuszcza dowolną ilość argumentów w implemetacji kontenerów STL jak długo będą one posiadały wartości domyślne). Autorzy D. Vandervoorde, N. Josuttis "C++ Szablony, Vademecum profesjonalisty" ostrzegają, że w tej sytuacji kompilator może nie zaakceptować wyrażenia:
stos<double,std::vector> sv;
ponieważ ignoruje fakt istnienia wartości domyślnej dla drugiego parametru szablonu std::vector. Mamy wtedy niezgodność pomiędzy przekazanym argumentem szablonu
template<typename T>
std::vector<T,typename A = std::allocator<T> >;
oraz deklaracją paremetru Sequence jako:
template<typename X > class Sequence ;
która zakłada tylko jeden parametr szablonu. Można wtedy zmienić deklarację szablonu stos i podać domyślny argument dla szablony w liście parametrów:
template<typename T,template<typename X ,typename A =
std::allocator<X> > class C > class stos {...}
W praktyce używane przeze mnie kompilatory (g++ wersja >= 3.3) nie wymagały takiej konstrukcji. Przyznaję, że nie udało mi się doczytać czy jest to cecha kompilatora g++, czy nowego standardu C++ (autorzy D. Vandervoorde, N. Josuttis "C++ Szablony, Vademecum profesjonalisty" opierali się na poprzednim wydaniu standardu).
Konkretyzacja na żądanie
Jak już wspomniałem wcześniej, konkretyzacja szablonów może odbywać się "na żądanie". W takim przypadku kompilator będzie konkretyzował tylko funkcje napotkane w kodzie. I tak, jeśli np. nie użyjemy w naszym kodzie funckji Stack<int>::pop(), to nie zostanie ona wygenerowana. Można z tego skorzystać i konkretyzować klasy typami, które nie spełniają wszystkich ograniczeń nałożonych na parametry szablonu. Wszystko bedzię w porządku jak długo nie będziemy używać funkcji łamiących te ograniczenia. Np. załóżmy, że do szablonu Stack dodajemy możliwość jego sortowania (wiem, to nie jest zgodne z duchem programowania obiektowego, stos nie posiada operacji sortowania, puryści mogą zastąpić ten przykład kontenerem list):
template<typename T,int N> void Stack<T,N>::sort() { bubble_sort(_rep,N); };
Możemy teraz np. używać
Stack<std::complex<double> > sc; sc.push( std::complex<double>(0,1)); sc.pop();
ale nie
sc.sort();
Natomiast konkretyzacja jawna
template Stack<std::complex<double> >;
nie powiedzie się, bo kompilator będzie się starał skonkretyzować wszystkie składowe klasy Stack, w tym metodę sort().
Typy stowarzyszone
W klasach poza metodami i polami możemy definiować również typy, które będziemy nazywali stowarzyszonymi z daną klasą. Jest to szczególnie przydatne w przypadku szablonów. Rozważmy następujący przykład:
template<typename T> Stack { public: typedef T value_type; ... }
Możemy teraz używać tej definicji w innych szablonach
template<typename S> void f(S s) { typename S::value_type total; słowo typename jest wymagane, inaczej kompilator założy, że S::value_type odnosi się do statycznej składowej klasy while(!s.is_empty() ) { total+=s.pop(); } return total; }
Bez takich możliwości musielibyśmy przekazać typ elementów stosu w osobnym argumencie. Mechanizm typów stowarzyszonych jest bardzo czesto używany w uogólnionym kodzie.