Zaawansowane CPP/Wykład 2: Programowanie uogólnione: Różnice pomiędzy wersjami
Matiunreal (dyskusja | edycje) Nie podano opisu zmian |
m Zastępowanie tekstu – „ </math>” na „</math>” |
||
(Nie pokazano 174 wersji utworzonych przez 8 użytkowników) | |||
Linia 5: | Linia 5: | ||
bezpośrednie, czyli z poziomu języka, wsparcie dla tworzenia | bezpośrednie, czyli z poziomu języka, wsparcie dla tworzenia | ||
uogólnionych funkcji i typów (nazywanych też funkcjami lub typami | uogólnionych funkcji i typów (nazywanych też funkcjami lub typami | ||
parametryzowanymi). Uogólnienie polega na tym że za jednym zamachem | parametryzowanymi). Uogólnienie polega na tym, że za jednym zamachem | ||
definiujemy całe rodziny klas lub funkcji. Po podstawieniu za | definiujemy całe rodziny klas lub funkcji. Po podstawieniu za | ||
parametry konkretnych argumentów szablonu dostajemy już egzemplarz | parametry konkretnych argumentów szablonu dostajemy już egzemplarz | ||
Linia 16: | Linia 16: | ||
Szablony okazały się bardzo silnym narzędziem, których zastosowanie | Szablony okazały się bardzo silnym narzędziem, których zastosowanie | ||
daleko przekracza implementację prostych kontenerów i można spokojnie | daleko przekracza implementację prostych kontenerów i można spokojnie | ||
stwierdzić że ich prawdziwy potencjał jest ciągle odkrywany. Szablony | stwierdzić, że ich prawdziwy potencjał jest ciągle odkrywany. Szablony | ||
idealnie wspierają styl programowania nazywany programowaniem | idealnie wspierają styl programowania nazywany programowaniem | ||
uogólnionym. Polega on na generalizowaniu algorytmów i struktur danych | uogólnionym. Polega on na generalizowaniu algorytmów i struktur danych | ||
tak aby były w dużej mierze niezależne od typów danych na których | tak, aby były w dużej mierze niezależne od typów danych, na których | ||
działają lub z których się składają. Mam nadzieję że po lekturze | działają lub z których się składają. Mam nadzieję, że po lekturze | ||
poprzedniego wykładu | poprzedniego wykładu Państwo już widzą, że to jest właśnie to, do czego | ||
szablony zostały wymyślone. Nie oznacza to że automatycznie każdy | szablony zostały wymyślone. Nie oznacza to, że automatycznie każdy | ||
program używajacy szablonów jest od razu programem uogólnionym. | program używajacy szablonów jest od razu programem uogólnionym. Tak | ||
jak i w przypadku tworzenia zwykłych (bez szablonów) programów, trzeba | |||
się sporo natrudzić aby uzyskać uniwersalny, łatwy do ponownego | się sporo natrudzić, aby uzyskać uniwersalny, łatwy do ponownego | ||
wykorzystania kod. Ten wykład ma właśnie za zadanie przekazać | wykorzystania kod. Ten wykład ma właśnie za zadanie przekazać Państwu | ||
podstawowe wiadomości na temat pisania dobrych programów uogólnionych. | podstawowe wiadomości na temat pisania dobrych programów uogólnionych. | ||
W programowaniu uogólnionym ważną rolę gra pojęcie konceptu. Koncept | W programowaniu uogólnionym ważną rolę gra pojęcie konceptu. Koncept | ||
to asbtrakcyjna definicja rodziny typów. To pojęcie | to asbtrakcyjna definicja rodziny typów. To pojęcie pełni podobną rolę | ||
jak interfejs w programowaniu uogólnionym, ale przynależność do tej | jak interfejs w programowaniu uogólnionym, ale przynależność do tej | ||
rodziny jest określona proceduralnie: do konceptu | rodziny jest określona proceduralnie: do konceptu należą typy, które | ||
spełniają pewne wymagania. Czyli jeśli coś kwacze jak kaczka | spełniają pewne wymagania. Czyli jeśli coś kwacze jak kaczka to | ||
jest kaczka, a nie: to jest kaczka jeśli należy do rodziny | jest to kaczka, a nie: to jest kaczka jeśli należy do rodziny | ||
"kaczowatych". Koncepty omówię w dalszej części tego wykładu. | "kaczowatych". Koncepty omówię w dalszej części tego wykładu. | ||
Linia 40: | Linia 40: | ||
przykładach niż opisać. Niewątpliwie najważniejszą i najbardziej | przykładach niż opisać. Niewątpliwie najważniejszą i najbardziej | ||
znaną aplikacją programowania ogólnego jest Standardowa Biblioteka | znaną aplikacją programowania ogólnego jest Standardowa Biblioteka | ||
Szablonów (STL | Szablonów (STL - Standard Template Library), będąca oficjalną częścią | ||
standardu C++. W tych wykladach będę się bardzo często posługiwał | standardu C++. W tych wykladach będę się bardzo często posługiwał | ||
przykładami z STL-a, ale szczegółowe | przykładami z STL-a, ale szczegółowe nauczenie posługiwania się tą | ||
biblioteką '''nie''' jest celem tego wykładu. Powinni jednak | biblioteką '''nie''' jest celem tego wykładu. Powinni jednak Państwo | ||
zrobić to sami. Dlatego zachęcam do analizy przykładów zamieszczonych | zrobić to sami. Dlatego zachęcam do analizy przykładów zamieszczonych | ||
na | na wykładzie oraz wykonywanie podanych ćwiczeń. | ||
Drugim znakomitycm źródłem przykladów uogólnionego kodu jest | Drugim znakomitycm źródłem przykladów uogólnionego kodu jest | ||
repozytorium bibliotek | repozytorium bibliotek [http://www.boost.org/ boost]. Stamtąd | ||
też będę podawał przykłady i znów gorąco zachęcam Państwa do | |||
zaglądania tam samemu. | zaglądania tam samemu. | ||
Linia 55: | Linia 55: | ||
choć oczywiście wymaga możliwości definiowania własnych typów. Oba | choć oczywiście wymaga możliwości definiowania własnych typów. Oba | ||
style programowania: uogólniony i obiektowy można oczywiście stosować | style programowania: uogólniony i obiektowy można oczywiście stosować | ||
razem. Każdy ma swoje charakterystyczne cechy | razem. Każdy ma swoje charakterystyczne cechy i aby je podkreślić | ||
jeszcze raz przypomnę podstawy programowania obiektowego rozumianego | jeszcze raz przypomnę podstawy programowania obiektowego rozumianego | ||
jako programowanie z użyciem interfejsów(klas abstrakcyjnych) i | jako programowanie z użyciem interfejsów(klas abstrakcyjnych) i | ||
Linia 63: | Linia 63: | ||
Sercem programowania obiektowego, oczywiście poza koncepcją klasy i | Sercem programowania obiektowego, oczywiście poza koncepcją klasy i | ||
obiektu jest polimorfizm dynamiczny, czyli możliwość decydowania o tym | obiektu, jest polimorfizm dynamiczny, czyli możliwość decydowania o tym | ||
jaka funkcja zostanie wywołana pod | jaka funkcja zostanie wywołana pod daną nazwą nie w momencie | ||
kompilacji (czyli pisania kodu) ale w samym momecie wywołania. | kompilacji (czyli pisania kodu), ale w samym momecie wywołania. | ||
Zilustrujemy to na przykładzie. W tym celu skorzystamy z | Zilustrujemy to na przykładzie. W tym celu skorzystamy z "matki | ||
wszystkich przykładów programowania obiektowego | wszystkich przykładów programowania obiektowego", czyli klasy | ||
kształtów graficznych:) | kształtów graficznych:). | ||
Problem jest następujący: nasz program w pewnym momencie musi | Problem jest następujący: nasz program w pewnym momencie musi | ||
manipulować kształtami graficznym: rysować, przesuwać, obracać itp. | manipulować kształtami graficznym: rysować, przesuwać, obracać itp. | ||
Jest w miarę oczywiste że każdy kształt będzie posiadał swoją klasę. | Jest w miarę oczywiste, że każdy kształt będzie posiadał swoją klasę. | ||
Następnym krokiem jest ocena które operacje w naszym kodzie wymagają | Następnym krokiem jest ocena które operacje w naszym kodzie wymagają | ||
szczególowej znajomości kształtu, a które tylko ogólnych jego | szczególowej znajomości kształtu, a które tylko ogólnych jego | ||
własności. Ewidentnie operacja rysowania obiektu należy do tych | własności. Ewidentnie operacja rysowania obiektu należy do tych | ||
pierwszych i musi być zdefiniowana w klasie danego kształtu. Mówimy że | pierwszych i musi być zdefiniowana w klasie danego kształtu. Mówimy, że | ||
"obiekt wie jak się narysować". Często mówi się o tym również jako o | |||
ustaleniu odpowiedzialności, czy o podziale obowiązków. Tak więc | ustaleniu odpowiedzialności, czy o podziale obowiązków. Tak więc | ||
ustaliliśmy, że do obowiązków obiektu należy | ustaliliśmy, że do obowiązków obiektu należy umiejętność narysowania | ||
się. Jeśli tak to właściwie cała część kodu manipulującego kształtami | się. Jeśli tak, to właściwie cała część kodu manipulującego kształtami | ||
nie musi znać szczegółów ich implementacji. Weźmy na przykład | nie musi znać szczegółów ich implementacji. Weźmy na przykład | ||
fragment aplikacji odpowiedzialny za odświeżanie ekranu, | fragment aplikacji odpowiedzialny za odświeżanie ekranu. Zakładamy, że | ||
wskaźniki do wyświetlanych kształtów są przechowywane w tablicy | wskaźniki do wyświetlanych kształtów są przechowywane w tablicy | ||
<tt>shape_table</tt>: | |||
for(size_t i=0;i<n;++i) | for(size_t i=0;i<n;++i) | ||
shape_table[i]->draw(); | shape_table[i]->draw(); | ||
<font color=red>kod źródłowy</font> | |||
Programista piszący ten kod nie musi wiedziec jakiego typu kształt | Programista piszący ten kod nie musi wiedziec jakiego typu kształt | ||
jest przechowywany w danym elemencie | jest przechowywany w danym elemencie tablicy <tt>shape_table</tt> i jak | ||
jest zaimplementowana funkcja | jest zaimplementowana funkcja <tt>draw</tt>. Istotne jest by każdy obiekt, | ||
którego wkaźnik przechowywany jest w tej tablicy posiadał metodę | którego wkaźnik przechowywany jest w tej tablicy posiadał metodę | ||
<tt>draw</tt>. Innymi słowy programista korzysta tu tylko ze znajomości i | |||
dostępności interfejsu obiektów | dostępności interfejsu obiektów typu kształt, a resztę wykonuje | ||
kompilator, który generuje kod zapewniający wywołanie odpowiedniej | kompilator, który generuje kod zapewniający wywołanie odpowiedniej | ||
funkcji. Aby taki interfejs zdefiniować tworzymy abstrakcyjną klasę | funkcji. Aby taki interfejs zdefiniować tworzymy abstrakcyjną klasę | ||
obiektów typu kształt: | obiektów typu kształt: | ||
class Shape { | class Shape { | ||
protected: | protected: | ||
long int _x; | long int _x; | ||
long int _y; | long int _y; | ||
public: | public: | ||
Shape(long x,long y):_x(x),_y(y){}; | Shape(long x,long y):_x(x),_y(y){}; | ||
long get_x() const {return _x;} | long get_x() const {return _x;} | ||
long get_y() const {return _y;} | long get_y() const {return _y;} | ||
virtual void draw() = 0; | virtual void draw() = 0; | ||
virtual ~Shape() {}; | virtual ~Shape() {}; | ||
}; | |||
([[media:Shape.h | Źródło shape.h]]) | |||
Klasa ta stanowić będzie klasę bazową dla wszystkich klas opisujących | Klasa ta stanowić będzie klasę bazową dla wszystkich klas opisujących | ||
kształty. Klasa | kształty. Klasa <tt>Shape</tt> jest klasą abstrakcyjną, ponieważ zawiera | ||
niezaimplementowaną wirtualną czystą fukcję <tt>void draw()</tt>. | |||
Kod definiujący konkretne klasy kształtów może | Kod definiujący konkretne klasy kształtów może wyglądać następująco: | ||
class Rectangle: public Shape { | class Rectangle: public Shape { | ||
protected: | protected: | ||
long _ur_x; | long _ur_x; | ||
long _ur_y; | long _ur_y; | ||
public: | public: | ||
Rectangle(long ll_x,long ll_y,long ur_x,long ur_y): | Rectangle(long ll_x,long ll_y,long ur_x,long ur_y): | ||
Shape(ll_x,ll_y),_ur_x(ur_x-ll_x),_ur_y(ur_y-ll_y) {}; | Shape(ll_x,ll_y),_ur_x(ur_x-ll_x),_ur_y(ur_y-ll_y) {}; | ||
virtual void draw() { | virtual void draw() { | ||
std::cerr<<"rectangle : "<<_x<<" "<<_y<<" : "; | std::cerr<<"rectangle : "<<_x<<" "<<_y<<" : "; | ||
std::cerr<<_ur_x+_x<<" "<<_ur_y+_y<<std::endl; | std::cerr<<_ur_x+_x<<" "<<_ur_y+_y<<std::endl; | ||
} | } | ||
long get_ur_x() const {return _ur_x;}; | long get_ur_x() const {return _ur_x;}; | ||
long get_ur_y() const {return _ur_y;}; | long get_ur_y() const {return _ur_y;}; | ||
}; | }; | ||
([[media:Rectangle.h | Źródło rectangle.h]]) | |||
i | i | ||
virtual void draw() { | class Circle: public Shape { | ||
std::cerr<<"Circle : "<<_x<<" "<<_y<<" : "<<_r<<std::endl; | protected: | ||
} | long _r; | ||
long get_r() const {return _r;}; | public: | ||
}; | Circle(long x, long y,long r) :Shape(x,y), _r(r) {}<br> | ||
virtual void draw() { | |||
std::cerr<<"Circle : "<<_x<<" "<<_y<<" : "<<_r<<std::endl; | |||
} | |||
long get_r() const {return _r;}; | |||
}; | |||
([[media:Circle.h | Źródło circle.h]]) | |||
Teraz możemy zdefiniować już funkcję odświeżającą ekran: | Teraz możemy zdefiniować już funkcję odświeżającą ekran: | ||
void draw_shapes(Shape *table[],size_t n) { | void draw_shapes(Shape *table[],size_t n) { | ||
for(size_t i=0;i<n;++i) | for(size_t i=0;i<n;++i) | ||
table[i]->draw(); | table[i]->draw(); | ||
} | } | ||
([[media:Draw.cpp | Źródło draw.cpp]]) | |||
Funkcja | |||
która funkcja | Funkcja <tt>draw_shapes</tt> wykorzystuje zachowanie polimorficzne: to | ||
która funkcja <tt>draw</tt> zostanie wywołana zależy od tego jaki | |||
konkretny kształt jest wskazywany przez element tablicy. | konkretny kształt jest wskazywany przez element tablicy. | ||
Łatwo się o tym przekonać wykonując np. następujący kod | Łatwo się o tym przekonać wykonując np. następujący kod | ||
int main() { | int main() { | ||
Shape *list[4]; | Shape *list[4]; | ||
list[0]=new Circle(0,0,100); | list[0]=new Circle(0,0,100); | ||
list[1]=new Rectangle(20,20,80,80); | list[1]=new Rectangle(20,20,80,80); | ||
list[2]=new Circle(10,10,100); | list[2]=new Circle(10,10,100); | ||
list[3]=new Rectangle(20,0,80,10); | list[3]=new Rectangle(20,0,80,10); | ||
draw_shapes(list,4); | draw_shapes(list,4); | ||
} | } | ||
<font color=red>kod źródłowy</font> | |||
W ten sposób zaimplementowaliśmy podstawowy paradygmat programowania | W ten sposób zaimplementowaliśmy podstawowy paradygmat programowania | ||
Linia 181: | Linia 183: | ||
==Polimorfizm statyczny== | ==Polimorfizm statyczny== | ||
Patrząc na kod funkcji <tt>draw_shapes</tt> możemy zauważyć, że korzysta on | |||
jedynie z własności posiadania przez wskazywane obiekty metody | jedynie z własności posiadania przez wskazywane obiekty metody | ||
<tt>draw()</tt>. To sygnatura, czyli typ parametru wywołania tej funkcji | |||
określa że musi to być wskaźnik na typ | określa, że musi to być wskaźnik na typ <tt>Shape</tt>. Z poprzedniego | ||
wykładu pamiętamy że możemy zrezygnować z wymuszania typu argumentu | wykładu pamiętamy, że możemy zrezygnować z wymuszania typu argumentu | ||
wywołania funkcji poprzez użycie szablonu funcji: | wywołania funkcji poprzez użycie szablonu funcji: | ||
template<typename T> void draw_template(T table[],size_t n) { | template<typename T> void draw_template(T table[],size_t n) { | ||
for(size_t i=0;i<n;++i) | for(size_t i=0;i<n;++i) | ||
table[i].draw(); | table[i].draw(); | ||
} | } | ||
([[media:Draw_template.h | Źródło draw_template.h]]) | |||
przechowywany typ posiadał metodę | Taką funkcję możemy wywołać dla dowolnej tablicy, byle tylko | ||
przechowywany typ posiadał metodę <tt>draw</tt>. Mogą to być obiekty typów | |||
<tt>Circle</tt> i <tt>Rectangle</tt> (nie <tt>Shape</tt>, obiekty klasy <tt>Shape</tt> | |||
nie istnieją!), ale też inne zupełnie z nimi nie związane. | nie istnieją!), ale też inne zupełnie z nimi nie związane. | ||
Ilustruje to poniższy przykład | Ilustruje to poniższy przykład: | ||
int main() { | class Drawable { | ||
Drawable table_d[1]={Drawable()}; | public: | ||
Circle table_c[2]={Circle(10,10),Circle(0,50)}; | void draw() {cerr<<"hello world!"<<endl;} | ||
};<br> | |||
int main() { | |||
Drawable table_d[1]={Drawable()}; | |||
Circle table_c[2]={Circle(10,10),Circle(0,50)};<br> | |||
draw_template(table_d,1); | |||
draw_template(table_c,2); | |||
} | |||
<font color=red>kod źródłowy</font> | |||
Korzystając z szablonów uzyskaliśmy więc również pewien efekt | Korzystając z szablonów uzyskaliśmy więc również pewien efekt | ||
zachowania polimorficznego. W przeciwieństwie do poprzedniego | zachowania polimorficznego. W przeciwieństwie do poprzedniego | ||
przykładu jest to polimorfizm statyczny: to kompilator zadecyduje na | przykładu jest to polimorfizm statyczny: to kompilator zadecyduje na | ||
podstawie typu tablicy jaką funkcję | podstawie typu tablicy jaką funkcję <tt>draw</tt> wywołać. Oczywiście w | ||
rozważanym przypadku to podejście jest całkowicie nieadekwatne, mamy | rozważanym przypadku to podejście jest całkowicie nieadekwatne, mamy | ||
bowiem do czynienia z niejednorodną rodziną kształtów a wybór | bowiem do czynienia z niejednorodną rodziną kształtów, a wybór | ||
konkretnych kształtów dokunuje się podczas wykonywania programu. | konkretnych kształtów dokunuje się podczas wykonywania programu. | ||
Podając przykład z szablonami chciałem tylko podkreślić różnice | Podając przykład z szablonami chciałem tylko podkreślić różnice | ||
pomiędzy tymi dwoma technikami. Przykłady kiedy to szablony okazują | pomiędzy tymi dwoma technikami. Przykłady kiedy to szablony okazują | ||
się lepszym rozwiązaniem zostały podane | się lepszym rozwiązaniem zostały podane w poprzednim wykładzie. | ||
==Polimorfizm statyczny vs. dynamiczny== | ==Polimorfizm statyczny vs. dynamiczny== | ||
Jak już wspomniałem każdy styl posiada swoje cechy które w zależności | Jak już wspomniałem każdy styl posiada swoje cechy, które w zależności | ||
od okoliczności mogą być postrzegane jako wady lub zalety. Poniżej | od okoliczności mogą być postrzegane jako wady lub zalety. Poniżej | ||
podaję | podaję zebrane głowne właściwości każdego podejścia. | ||
# Dziedzieczenie i funkcje wirtualne | # Dziedzieczenie i funkcje wirtualne | ||
## umożliwia pracę ze zbiorami niejednorodnych obiektów i korzysta z polimorfizmu dynamicznego | |||
## | ## wymaga wspólnej hierarchii dziedziczenia | ||
z polimorfizmu dynamicznego | ## wymusza korzystanie ze wskaźników lub referencji i funkcji wirtualnych | ||
## zazwyczaj generuje mniejszy kod. | |||
## | |||
## | |||
wirtualnych | |||
## | |||
# Szablony | # Szablony | ||
## implementuje polimorfizm statyczny | |||
## bezpiecznie obsługuje jednorodne zbiory obiektów | |||
## nie trzeba korzystać ze wskaźników i referencji ani funkcji wirtualnych | |||
## nie musimy korzystać ze wspólnej hierarchii dziedziczenia. | |||
==Koncepty== | |||
Przyjrzyjmy się jeszcze raz deklaracji funkcji <tt>draw_shapes</tt> i | |||
<tt>draw_template</tt>. Kiedy programista widzi deklarację: | |||
void draw_shapes(Shape *table[]) | |||
wie, że interfejs wykorzystywany przez funkcję <tt>draw</tt> jest | |||
zdefiniowany przez klasę <tt>Shape</tt>. Aby go poznać musi przeczytać kod | |||
i dokumentację tej klasy. | |||
Natomiast kiedy programista widzi deklarację: | |||
template<typename T> void draw_template(T table[],size_t n); | |||
to musi prześledzić kod funkcji <tt>draw_templates</tt> aby poznać | |||
ograniczenia nałożone na argument szablonu <tt>T</tt>. W tym przypadku nie | |||
jest to trudne, ale ogólnie może to być nietrywialne zadanie. | |||
ograniczenia nałożone na argument szablonu | |||
jest to trudne, ale ogólnie może to być zadanie | |||
Zamiast jednak definiować ograniczenia i warunki dla każdego szablonu | Zamiast jednak definiować ograniczenia i warunki dla każdego szablonu | ||
Linia 276: | Linia 267: | ||
abstrakcyjną definicję całej rodziny typów, niezależną od konkretnego | abstrakcyjną definicję całej rodziny typów, niezależną od konkretnego | ||
szablonu. Typ spełniający warunki konceptu nazywamy modelem konceptu | szablonu. Typ spełniający warunki konceptu nazywamy modelem konceptu | ||
lub mówimy że modeluje ten koncept. Mając wybrany, dobrze | lub mówimy, że modeluje ten koncept. Mając wybrany, dobrze | ||
przygotowany zestaw konceptów dla danej dziedziny, możemy się nimi | przygotowany zestaw konceptów dla danej dziedziny, możemy się nimi | ||
posługiwać przy definiowaniu typów i algorytmów uogólnionych. | posługiwać przy definiowaniu typów i algorytmów uogólnionych. | ||
Koncepty mogą tworzyć hierachie analogiczne do | Koncepty mogą tworzyć hierachie analogiczne do hierarachii | ||
dziedziecznia. Mówimy że koncept | dziedziecznia. Mówimy, że koncept <tt>A</tt> jest bardziej wyspecjalizowany | ||
niż | niż <tt>B</tt> (<tt>A</tt> is-refinement-of <tt>B</tt>), jeśli zestaw ograniczeń | ||
konceptu | konceptu <tt>B</tt> zawiera się w zestwie ograniczeń konceptu <tt>A</tt>. | ||
Będę też używał określenia | Będę też używał określenia <tt>A</tt> jest "uszczegółowieniem" <tt>B</tt>. | ||
Pojęcie konceptu | Pojęcie konceptu pełni więc przy programowaniu za pomocą szablonów | ||
podobną rolę jak pojęcie interfejsu przy programowaniu za pomocą | podobną rolę jak pojęcie interfejsu przy programowaniu za pomocą | ||
abstrakcyjnych klas bazowych i polimorfizmu dynamicznego. W | abstrakcyjnych klas bazowych i polimorfizmu dynamicznego. W | ||
przeciwieństwie do interfejsu jest to jednak pojęcie bardziej | przeciwieństwie do interfejsu jest to jednak pojęcie bardziej | ||
"ulotne", bo nie narzucamy go za pomocą formalnej definicji klasy | |||
abstrakcyjnej. Koncepty definiujemy poprzez mniej lub bardziej | abstrakcyjnej. Koncepty definiujemy poprzez mniej lub bardziej | ||
ścisłe wypisanie nakładanych przez nie ograniczeń. Ograniczenia te | ścisłe wypisanie nakładanych przez nie ograniczeń. Ograniczenia te | ||
mogą zawierać między innymi: | mogą zawierać między innymi: | ||
# Prawidłowe wyrażenia. Zestaw wyrażeń języka C++ które muszą się | # Prawidłowe wyrażenia. Zestaw wyrażeń języka C++, które muszą się poprawnie kompilować. | ||
poprawnie kompilować | # Typy stowarzyszone. Ewentualne dodatkowe typy występujące w prawidłowych wyrażeniach. | ||
# Semantyka: zanczenie wyrażeń. Jednym ze sposobów określanie semantyki jest podawanie niezmienników, czyli wyrażeń, które dla danego konceptu są zawsze prawdziwe. | |||
# Złożoność algorytmów. Gwarancje co do czasu i innych zasobów potrzebnych do wykonania danego wyrażenia. | |||
Programowanie uogólnione polega więc na wyszukiwaniu konceptów na | |||
tyle ogólnych, aby pasowały do dużej liczby typów i na tyle | |||
szczegółowych, aby zezwalały na wydajną implementację. | |||
===Definiowanie konceptów=== | |||
Weźmy za przykład szablon funkcji <tt>max</tt> z poprzedniego wykładu | |||
template<typename T> max(T a,T b) {return (a>b)?a:b;} | |||
i zastanówmy się, jakie koncepty możemy odkryć w tak prostym | |||
i zastanówmy się jakie koncepty możemy odkryć w tak prostym | |||
kodzie. | kodzie. | ||
Zacznijmy od gramatyki. Jakie warunki musi spełniać typ | Zacznijmy od gramatyki. Jakie warunki musi spełniać typ <tt>T</tt>, aby | ||
podstawienie go jako argument szablonu | podstawienie go jako argument szablonu <tt>max</tt> dawało poprawne | ||
wyrażenie? Oczywistym warunkiem jest że dla tego typu musi być | wyrażenie? Oczywistym warunkiem jest, że dla tego typu musi być | ||
zdefiniowany operator porównania | zdefiniowany operator porównania <tt>bool operator>(...)</tt>. Specjalnie | ||
nie wyspecyfikowałem sygnatury tego operatora. Nie ma np. znaczenia | nie wyspecyfikowałem sygnatury tego operatora. Nie ma np. znaczenia | ||
jak parametry są przekazywane, co więcej | jak parametry są przekazywane, co więcej <tt>operator>(...)</tt> może być | ||
zdefiniowany jako składowa klasy i posiadać tylko jeden jawny argument. | |||
Ważne jest to że jeśli | Ważne jest to, że jeśli <tt>x</tt> i <tt>y</tt> są obiektami typu <tt>T</tt> | ||
to wyrażenie: | to wyrażenie: | ||
x>y | x>y | ||
jest poprawne (skompiluje się). | jest poprawne (skompiluje się). | ||
Łatwiej jest przeoczyć fakt | Łatwiej jest przeoczyć fakt, | ||
że ponieważ argumenty wywołania są zwracane i przekazywane przez | że ponieważ argumenty wywołania są zwracane i przekazywane przez | ||
wartość to typ | wartość, to typ <tt>T</tt> musi posiadać konstruktor kopiujący. Oznacza to, że | ||
jeśli | jeśli <tt>x</tt> i <tt>y</tt> są obiektami typu <tt>T</tt> to wyrażenia: | ||
Spełnienie | T(x); | ||
T x(y); | |||
T x = y; | |||
są poprawne. | |||
{{kotwica|przyklad.2.1|}}{{przyklad|2.1||}} | |||
Spełnienie obydwu tych warunków zapewni nam poprawność gramatyczną | |||
wywołania szablonu z danym typem, tzn. kod się skompiluje. | wywołania szablonu z danym typem, tzn. kod się skompiluje. | ||
A co z | A co z poprawnością semantyczną? Mogłoby sie wydawać, że jest bez znaczenia | ||
jak zdefiniujemy | jak zdefiniujemy <tt>operator>(...)</tt>. | ||
Koncept typu | Koncept typu <tt>T</tt> jest jednak częścią kontraktu dla | ||
funkcji | funkcji <tt>max</tt>. Kontraktu zawieranego pomiędzy twórcą tego wielce | ||
skomplikowanego kodu, a jego użytkownikiem. Kontrakt stanowi że jeżeli | skomplikowanego kodu, a jego użytkownikiem. Kontrakt stanowi, że jeżeli | ||
użytkownik dostarczy do funkcji argumenty | użytkownik dostarczy do funkcji argumenty o typach zgodnych z | ||
konceptem i o wartościach spełniających być może inne warunki wstępne, | konceptem i o wartościach spełniających być może inne warunki wstępne, | ||
to twórca funkcji gwarantuje że zwróci ona poprawny wynik. | to twórca funkcji gwarantuje, że zwróci ona poprawny wynik. | ||
Zastanówny się więc jak zdefiniować poprawność dla funkcji maksimum. Z | Zastanówny się więc jak zdefiniować poprawność dla funkcji maksimum. Z | ||
definicji maksimum żaden element argument funkcji | definicji maksimum żaden element argument funkcji <tt>max</tt> nie może być | ||
większy od wyniku, czyli wyrażenie | większy od wyniku, czyli wyrażenie | ||
{{kotwica|prz.2.1|}} | |||
<center><math> | |||
!( a> max(a,b) ) \wedge !(b> max(a,b)) \quad\mbox{(2.1)} | |||
</math></center> | </math></center> | ||
musi być zawsze prawdziwe. Jasne jest że jeśli dla jakiegoś typu | musi być zawsze prawdziwe. Jasne jest, że jeśli dla jakiegoś typu | ||
<tt>X</tt> zdefiniujemy operator porównania tak, aby zwracał zawsze prawdę | |||
bool operator>(const X &a,const X &b) {return 1;} | bool operator>(const X &a,const X &b) {return 1;} | ||
lub aby był równoważny operatorowi | lub aby był równoważny operatorowi równości: | ||
bool operator>(const X &a,const X &b) {return a==b;} | bool operator>(const X &a,const X &b) {return a==b;} | ||
to wyrażenie [[# | to wyrażenie [[#prz.2.1|2.1]] nie może być prawdziwe dla żadnej | ||
wartości | wartości <tt>a</tt> i <tt>b</tt>. Aby funkcja <tt>max</tt> mogła spełnić swój | ||
warunek końcowy musimy | warunek końcowy musimy narzucić pewne ograniczenia semantyczne na | ||
<tt>operator>()</tt>. Te warunki to żądanie, aby relacja większości | |||
definiowana przez ten operator byłą relacją porządku częściowego, a | definiowana przez ten operator byłą relacją porządku częściowego, a | ||
więc aby spełnione było | więc aby spełnione było | ||
< | <center><math>(x>x) = false</math> i <math>(x>y) \wedge (y>z) => (x>z)</math></center> | ||
(x>y) | |||
< | |||
To rozumowanie | To rozumowanie możnaby ciągnąć dalej i zauważyć, że nawet z tym | ||
ograniczeniem uzyskamy nieintuicyjne wyniki w przypadku gdy obiekty | ograniczeniem uzyskamy nieintuicyjne wyniki w przypadku, gdy obiekty | ||
<tt>a</tt> i <tt>b</tt> będą nieporównywalne, tzn. <math>!(a>b)</math> i <math>!(b>a)</math>. | |||
Poprawność semantyczną konstruktora kopiującego jest trudniej | Poprawność semantyczną konstruktora kopiującego jest trudniej | ||
zdefiniować, ograniczymy się więc tylko do stwierdzenia że wykonanie | zdefiniować, ograniczymy się więc tylko do stwierdzenia, że wykonanie | ||
operacji [[# | operacji [[#prz.2.1|2.1]] powoduje powstanie kopii obiektu <tt>x</tt> | ||
( | (cokolwiek by to nie znaczyło). | ||
===Comparable i Assignable=== | ===Comparable i Assignable=== | ||
Reasumując, dostajemy zbiór warunków, które musi | |||
spełniać typ <tt>T</tt>, aby móc go podstawić do szablonu funkcji <tt>max</tt>. | |||
spełniać typ | Czy to oznacza, że zdefiniowaliśmy już poprawny koncept? Żeby się o tym | ||
Czy to oznacza że zdefiniowaliśmy już poprawny koncept? Żeby się o tym | |||
przekonać spróbujmy go nazwać. Narzuca się nazwa w stylu | przekonać spróbujmy go nazwać. Narzuca się nazwa w stylu | ||
<tt>Comparable</tt>, ale wtedy łatwo zauważyć, że istnienie konstruktora | |||
kopiującego nie ma z tym nic wspólnego. Próbujemy upchnąc dwa | kopiującego nie ma z tym nic wspólnego. Próbujemy upchnąc dwa | ||
niezależne pojęcia do jednego worka. Co więcej bardzo łatwo jest | niezależne pojęcia do jednego worka. Co więcej bardzo łatwo jest | ||
zrezygnować z konieczności | zrezygnować z konieczności posiadania konstruktora kopiujacego, | ||
zmieniając deklarację | zmieniając deklarację <tt>max</tt> na: | ||
template<typename T> const T& max(const T&,const T&); | template<typename T> const T& max(const T&,const T&); | ||
Teraz argumenty i wartość zwracana przekazywane są przez referencję i | |||
nie ma potrzeby kopiowania obiektów. | nie ma potrzeby kopiowania obiektów. | ||
Logiczne jest więc wydzielenie dwu konceptów: jednego definiującego | Logiczne jest więc wydzielenie dwu konceptów: jednego definiującego | ||
typy porównywalne, drugiego typy | typy porównywalne, drugiego - typy "kopiowalne". Dalej możemy | ||
zauważyć że istnienie operatora > automatycznie pozwala na | zauważyć, że istnienie operatora <tt>></tt> automatycznie pozwala na | ||
zdefiniowanie | zdefiniowanie operatora <tt><</tt> poprzez: | ||
bool operator<(const T& a,const T&b) {return b>a;}; | bool operator<(const T& a,const T&b) {return b>a;}; | ||
Podobnie istnienie konstruktora kopiującego jest blisko związane z | Podobnie istnienie konstruktora kopiującego jest blisko związane z | ||
istnieniem operatora przypisania. | istnieniem operatora przypisania. | ||
Tak więc dochodzimy do dwu konceptów: | Tak więc dochodzimy do dwu konceptów: <tt>Comparable</tt> reprezentującego | ||
typy których obiekty można porównywać za pomocą operatorów | typy, których obiekty można porównywać za pomocą operatorów <tt><</tt> i | ||
<tt>></tt> oraz <tt>Assignable</tt> reprezentujacego typy, których | |||
obiekty możemy kopiować i przypisywać do siebie | obiekty możemy kopiować i przypisywać do siebie. Taką | ||
zabawę można kontynuować, pytając np. co z operatorem porównania | zabawę można kontynuować, pytając np. co z operatorem porównania | ||
<tt>operator==()</tt>?, co z konstruktorem defaultowym? itd. Widać więc, że | |||
koncepty to sprawa subietywna, ale to żadna nowość. Wybór używanych | koncepty to sprawa subietywna, ale to żadna nowość. Wybór używanych | ||
abstrakcji jest zawsze sprawą mniej lub bardziej subiektywną i silnie | abstrakcji jest zawsze sprawą mniej lub bardziej subiektywną i silnie | ||
zależną od rozpatrywanego problemu. O tym czy dwa pojęcia | zależną od rozpatrywanego problemu. O tym czy dwa pojęcia włączymy do | ||
jednego konceptu czy nie decyduje np. | jednego konceptu czy nie decyduje np. odpowiedź na pytanie czy | ||
prawdopodobne jest użycie kiedykolwiek któregoś z tych pojęć osobno? | prawdopodobne jest użycie kiedykolwiek któregoś z tych pojęć osobno? | ||
Linia 438: | Linia 420: | ||
kontekście je rozpatrujemy. Na tym wykladzie kontekstem jest STL i oba | kontekście je rozpatrujemy. Na tym wykladzie kontekstem jest STL i oba | ||
wprowadzone koncepty są wzorowane na koncetach z | wprowadzone koncepty są wzorowane na koncetach z | ||
STL-a. Należy jednak nadmienić że pojęcie | STL-a. Należy jednak nadmienić, że pojęcie | ||
konceptu nie pojawia się wprost w definicji stadardu C++. Najlepiej | konceptu nie pojawia się wprost w definicji stadardu C++. Najlepiej | ||
koncepty STL | koncepty STL przedstawione są na stronach firmy [http://www.sgi.com/tech/stl/ SGI] dokąd Państwa odsyłam. | ||
dokąd | |||
==STL== | ==STL== | ||
Linia 448: | Linia 429: | ||
programistyczne zawarte w standardzie C++. Stanowi ona również | programistyczne zawarte w standardzie C++. Stanowi ona również | ||
znakomity, niejako sztandarowy, przykład programowania uogólnionego. | znakomity, niejako sztandarowy, przykład programowania uogólnionego. | ||
Na | Na tę bibliotekę można patrzeć więc dwojako: jako rozszerzenie języka | ||
C++ | C++ o dodatkowe funkcje lub jako na zbiór konceptów stanowiących | ||
podstawę do projetowania programów uogólnionych. Ja chciałbym | podstawę do projetowania programów uogólnionych. Ja chciałbym | ||
podkreślić ten drugi aspekt, podkreślając jednak że dobre poznanie | podkreślić tutaj ten drugi aspekt, podkreślając jednak, że dobre poznanie | ||
możliwości STL-a może bardzo ułatwić | możliwości STL-a może bardzo ułatwić Państwu prace programistyczne. | ||
Biblioteka składa się zasadniczo z dwu części: uogólnionych kontenerów | Biblioteka składa się zasadniczo z dwu części: uogólnionych kontenerów | ||
i uogólnionych algorytmów. Trzecią cześcią niejako sklejającą te dwie są | i uogólnionych algorytmów. Trzecią cześcią, niejako sklejającą te dwie, są | ||
iteratory. | iteratory. | ||
Linia 463: | Linia 444: | ||
programowania uogólnionego jest jednak sprawa ujednolicenia dostępu do | programowania uogólnionego jest jednak sprawa ujednolicenia dostępu do | ||
zawartości kontenera. Rozważmy dla przykładu dwa typowe kontenery | zawartości kontenera. Rozważmy dla przykładu dwa typowe kontenery | ||
<tt>vector</tt> i <tt>list</tt>, implementujące odpowiednio "inteligentną" | |||
tablicę oraz listę dwukierunkową. Naturalnym sposobem dostępu do | tablicę oraz listę dwukierunkową. Naturalnym sposobem dostępu do | ||
tablicy jest indeksowanie: | tablicy jest indeksowanie: | ||
std::vector<int> v(10); | std::vector<int> v(10); | ||
v[8]<nowiki>=</nowiki>1; | v[8]<nowiki>=</nowiki>1; | ||
a listy przeglądamy po kolei, przesuwając się o jeden element w przód czy w tył | |||
l.current()<nowiki>=</nowiki>1; | <i>Uwaga! To nie jest kod STL-owy !!!</i> | ||
lista<int> l; | |||
l.reset(); <i>ustawia element bieżacy na początek listy</i> | |||
for(int i<nowiki>=</nowiki>0;i<8;i++) | |||
l.next(); <i>przesuwa element bieżący o jeden element do przodu</i><br> | |||
l.current()<nowiki>=</nowiki>1; <i>zwraca referencję do elementu bieżącego</i> | |||
Widać że w takim sformułowaniu praktycznie nie jest możliwe napisanie | Widać, że w takim sformułowaniu praktycznie nie jest możliwe napisanie | ||
ogólnego kodu np. dodającego wszystkie elementy kontenera czy | ogólnego kodu np. dodającego wszystkie elementy kontenera czy | ||
wyszukującego jakiś element w kontenerze. Ponadto opisany sposób | wyszukującego jakiś element w kontenerze. Ponadto opisany sposób | ||
Linia 494: | Linia 474: | ||
przeglądać następująco | przeglądać następująco | ||
std::list<int> l; | std::list<int> l; | ||
<i>tu jakoś inicjalizujemy liste</i> | |||
for(list<int>::iterator it<nowiki>=</nowiki>l.begin();it!<nowiki>=</nowiki>l.end();it++) { | for(list<int>::iterator it<nowiki>=</nowiki>l.begin();it!<nowiki>=</nowiki>l.end();it++) { | ||
<i>każdy kontener definiuje typ stowarzyszony nazwany <tt>iterator</tt></i> | |||
cout<<*it<<endl; | cout<<*it<<endl; | ||
<i>korzystamy z iteratorów jak ze zwykłych wskaźników</i> | |||
} | } | ||
} | } | ||
Przykładowy ogólny algorytm oparty o iteratory może wyglądać w ten sposób: | Przykładowy ogólny algorytm oparty o iteratory może wyglądać w ten sposób: | ||
template <class InputIterator, class T> | template <class InputIterator, class T> | ||
T accumulate(InputIterator first, InputIterator last, T init) { | T accumulate(InputIterator first, InputIterator last, T init) { | ||
T total<nowiki>=</nowiki>init; | T total<nowiki>=</nowiki>init; | ||
for( ; first !<nowiki>=</nowiki> last;++first) | for( ; first !<nowiki>=</nowiki> last;++first) | ||
total+<nowiki>=</nowiki> *first; | total+<nowiki>=</nowiki> *first; | ||
return total; | return total; | ||
} | } | ||
([[media:Accumulate.cpp | Źródło: accumulate.cpp]]) | |||
Oczywiście nie da się zignorować fundamentalnych różnic pomiędzy listą | Oczywiście nie da się zignorować fundamentalnych różnic pomiędzy listą | ||
a wektorem. Dlatego np. iterator | a wektorem. Dlatego np. iterator wektora zezwala na konstrukcje | ||
<tt>it[i]</tt>, a iterator listy już nie. Oznacza to, że algorytm, który | |||
działa dla iteratorów wektora (np. | działa dla iteratorów wektora (np. <tt>sort</tt>), nie musi działać dla | ||
iteratora listy. W języku konceptów oznacza to że | iteratora listy. W języku konceptów oznacza to, że | ||
<tt>std::vector<T>::iterator</tt> jest modelem konceptu bardziej | |||
wyspecjalizowanego niż koncept którego modelem jest | wyspecjalizowanego niż koncept, którego modelem jest | ||
<tt>std::list<T>::iterator</tt>. Zobaczymy to w następnej części tego | |||
wykładu. | wykładu. | ||
Linia 528: | Linia 509: | ||
Standard C++ definiuje dwa zestawy kontenerów wchodzące w skład STL: | Standard C++ definiuje dwa zestawy kontenerów wchodzące w skład STL: | ||
# Sekwencje czyli pojemniki w których kolejność elementów jest | #Sekwencje czyli pojemniki, w których kolejność elementów jest ustalana przez korzystającego z pojemnika (klienta) są to: | ||
ustalana przez korzystającego z pojemnika (klienta) są to: | ## [http://www.sgi.com/tech/stl/Vector.html <tt>vector</tt>] | ||
## [http://www.sgi.com/tech/stl/Deque.html <tt>deque</tt>] | |||
## [http://www.sgi.com/tech/stl/List.html <tt>list</tt>] | |||
# Kontenery asocjacyjne, czyli pojemniki, w których klient nie ma kontroli nad kolejnością elementów, są to: | |||
## [http://www.sgi.com/tech/stl/set.html <tt>set</tt>] | |||
## [http://www.sgi.com/tech/stl/Map.html <tt>map</tt>] | |||
## [http://www.sgi.com/tech/stl/multiset.html <tt>multiset</tt>] | |||
## [http://www.sgi.com/tech/stl/Multimap.html <tt>multimap</tt>] | |||
Ponadto różni dostawcy oferują dodatkowe pojemniki. Na uwagę zasługuje | |||
znakomita darmowa implementacja STL firmy Silicon Graphics, która | |||
miedy innymi wchodzi w skład pakietu g++ i dostarcza dodatkowo takich | |||
kontenerów jak: lista jednokierunkowa <tt>slist</tt> oraz tablice | |||
haszujące <tt>hash_set</tt> czy <tt>hash_map</tt> (zob. [http://www.sgi.com/tech/stl/ STL]). Hierachię | |||
konceptów kontenerów typu sekwencji przedstawia [[#rys.2.1|rysunek 2.1]], a kontenerów asocjacyjnych [[#rys.2.2|rysunek 2.2]]. | |||
{{kotwica|rys.2.1|}}<center> | |||
[[File:cpp-2-sequence.svg|600x400px|thumb|center|Rysunek 2.1. Hierarchia konceptów dla pojemników typu sekwencyjnego.]] | |||
</center> | |||
{{kotwica|rys.2.2|}}<center> | |||
[[File:cpp-2-associate.svg|600x400px|thumb|center|Rysunek 2.2. Hierarchia konceptów dla pojemników typu asocjacyjnego.]] | |||
</center> | |||
Nie będę tu omawiał tych wszystkich konceptów. Ich szczegółowe opisy | |||
znajdują się na stronie [http://www.sgi.com/tech/stl/ http://www.sgi.com/tech/stl/]. Tutaj chciałbym tylko dodać parę luźnych | |||
Nie będę tu omawiał tych wszystkich konceptów. Ich | |||
znajdują się na stronie | |||
komentarzy. | komentarzy. | ||
Po pierwsze rodzi się pytanie czy taka skomplikowana | Po pierwsze, rodzi się pytanie czy taka skomplikowana taksonomia jest | ||
potrzebna? W końcu patrząc na rysunki widać że konceptów jest dużo | potrzebna? W końcu patrząc na rysunki widać, że konceptów jest dużo | ||
więcej niż typów kontenerów. | więcej niż typów kontenerów. Rzeczywiście, do posługiwania się biblioteką | ||
w zasadzie wystarczy zaznajomić się z opisami kontenerów i | w zasadzie wystarczy zaznajomić się z opisami kontenerów i hierarchią | ||
iteratorów ( | iteratorów (zob. [[#rys.2.3|rysunek 2.3]]). Podane klasyfikacje przydają się dopiero kiedy dodajemy własne elementy do biblioteki. | ||
przydają się dopiero kiedy dodajemy własne elementy do biblioteki. | |||
Dobierając do implemetacji najbardziej ogólny koncept spełniający | Dobierając do implemetacji najbardziej ogólny koncept spełniający | ||
nasze wymagania zwiększamy potencjał ponownego użycia naszego kodu z | nasze wymagania zwiększamy potencjał ponownego użycia naszego kodu z | ||
innymi | innymi komponentami biblioteki, czy kodem innych developerów. | ||
Kontenery z STL są właścicielami swoich elementów, zniszczenie | Kontenery z STL są właścicielami swoich elementów, zniszczenie | ||
kontenera powoduje zniszczenie jego elementów. Wszytkie operacje | kontenera powoduje zniszczenie jego elementów. Wszytkie operacje | ||
wkładania elementów do kontenera używają przekazywania przez wartość, | wkładania elementów do kontenera używają przekazywania przez wartość, | ||
czyli kopiują wkładany obiekt. Jeżeli chcemy aby czas życia elementów | czyli kopiują wkładany obiekt. Jeżeli chcemy, aby czas życia elementów | ||
kontenera był dłuższy od czasu życia kontenera należy użyć wskaźników. | kontenera był dłuższy od czasu życia kontenera, należy użyć wskaźników. | ||
Kontenery różnią się nie tylko rodzajem iteratorów jaki implementują, | Kontenery różnią się nie tylko rodzajem iteratorów, jaki implementują, | ||
ale również rodzajem operacji które można wykonać bez unieważnienia | ale również rodzajem operacji, które można wykonać bez unieważnienia | ||
istniejących iteratorów. Pokażę to na przykładzie: | istniejących iteratorów. Pokażę to na przykładzie: | ||
std::vector<int>::iterator it; | std::vector<int>::iterator it; | ||
int i; | int i;<br> | ||
std::vector<int> v(1); | |||
std::vector<int> v(1); | std::vector<int> buff(100); <i>staramy się zająć pamięć za v</i><br> | ||
std::vector<int> buff(100); | v[0]<nowiki>=</nowiki>0; | ||
it<nowiki>=</nowiki>v.begin(); | |||
v[0]<nowiki>=</nowiki>0; | i<nowiki>=</nowiki>(*it); <i>OK, przypisuje i<nowiki>=</nowiki>0</i> | ||
it<nowiki>=</nowiki>v.begin(); | for(int i<nowiki>=</nowiki>0;i<10;++i) | ||
i<nowiki>=</nowiki>(*it); | v.push_back(i); | ||
for(int i<nowiki>=</nowiki>0;i<10;++i) | <i>ponieważ przekraczamy koniec wektora, kontener zaalokuje dodatkową pamięć. Może | ||
v.push_back(i); | się to wiązać z koniecznośćią przeniesienia zawartości wektora v w inne miejsce | ||
pamięci. To spowoduje, że wskaźnik it przestanie pokazywać na początek wektora v</i><br> | |||
dodatkową | std::cerr<<(*it)<<std::endl ; <i>niezdefiniowane</i><br> | ||
zawartości wektora v w inne miejsce pamięci. | std::cerr<<"iterator nieprawidlowy"<<std::endl; | ||
To spowoduje że wskaźnik it przestanie pokazywać na początek wektora v | for(;it !<nowiki>=</nowiki> v.end(); ++it) <i>potencjalnie nieskończona pętla</i> | ||
std::cerr<<*it<<std::endl; | |||
;<br> | |||
std::cerr<<(*it)<<std::endl ; | std::cerr<<"iterator prawidlowy"<<std::endl; | ||
for(it<nowiki>=</nowiki>v.begin();it !<nowiki>=</nowiki> v.end(); ++it) | |||
std::cerr<<"iterator nieprawidlowy"<<std::endl; | std::cerr<<*it<<std::endl; | ||
for(;it !<nowiki>=</nowiki> v.end(); ++it) | ; | ||
std::cerr<<*it<<std::endl; | |||
; | |||
([[media:Invalid.cpp | Źródło: invalid.cpp]]) | |||
Bardzo | Bardzo Państwa na ten problem uczulam. Efekt działania powyższego kodu | ||
jest gorzej niż zły: jest niezdefiniowany! tzn. będzie zależał od | jest gorzej niż zły: jest niezdefiniowany!, tzn. będzie zależał od | ||
implementacji kompilatora, od zadeklarownych wcześniej zmiennych itp. | implementacji kompilatora, od zadeklarownych wcześniej zmiennych itp. | ||
Proszę np. spróbować wykomentować linijkę | Proszę np. spróbować wykomentować linijkę | ||
std::vector<int> buff(100); | std::vector<int> buff(100); <i>staramy się zająć pamięć za v</i> | ||
i porównać wynik działania programu. Może się również zdarzyć że | i porównać wynik działania programu. Może się również zdarzyć, że | ||
program zadziała poprawnie | program zadziała poprawnie (wbrew pozorom jest to najgorsza | ||
możliwa sytuacja | możliwa sytuacja!). | ||
Ważne są gwarancje złożoności metod kontenera. Ewidentnie każdy | Ważne są gwarancje złożoności metod kontenera. Ewidentnie każdy | ||
rodzaj kontenera może dostarczyć każdego rodzaju operacji, różny | rodzaj kontenera może dostarczyć każdego rodzaju operacji, różny | ||
będzie jednak czas ich wykonywania. I tak | będzie jednak czas ich wykonywania. I tak rząd O(1) jest gwarantowany w operacji indeksowania | ||
wektora. Natomiast operacja dodania | |||
elementu w środku wektora jest rzędu O(N). Z listą jest odwrotnie i | elementu w środku wektora jest rzędu O(N). Z listą jest odwrotnie i | ||
dlatego listy w STL nie | dlatego listy w STL nie posiadają operacji indeksowania. | ||
Nie wszystkie własności kontenerów są zdefiniowane w konceptach. | Nie wszystkie własności kontenerów są zdefiniowane w konceptach. | ||
Linia 648: | Linia 605: | ||
===Iteratory=== | ===Iteratory=== | ||
Iteratory to koncept który uogólnia pojęcie wskaźnika. | Iteratory to koncept, który uogólnia pojęcie wskaźnika. | ||
Hierarchię konceptów iteratorów przedstawia | Hierarchię konceptów iteratorów przedstawia [[#rys.2.3|rysunek 2.3]]. Zaznaczono na nim również które | ||
koncepty kontenerów wymagają danego modelu iteratora. | koncepty kontenerów wymagają danego modelu iteratora. | ||
{Hierarchia konceptów dla iteratorów | {{kotwica|rys.2.3|}}<center> | ||
[[File:cpp-2-iteratory.svg|600x400px|thumb|center|Rysunek 2.3. Hierarchia konceptów dla iteratorów.]] | |||
</center> | |||
Najprostsze iteratory pojawiające sie w STL-u to iteratory wejściowe i | Najprostsze iteratory pojawiające sie w STL-u to iteratory wejściowe i | ||
wyjściowe. Wprawdzie żaden kontener nie posiada iteratorów tego | wyjściowe. Wprawdzie żaden kontener nie posiada iteratorów tego typu, | ||
ale iteratory wejściowe, umożliwiające tylko jednoprzebiegowe | ale iteratory wejściowe, umożliwiające tylko jednoprzebiegowe | ||
odczytanie wartości kontenera, są częstym wymaganiem dla argumentów | odczytanie wartości kontenera, są częstym wymaganiem dla argumentów | ||
Linia 665: | Linia 620: | ||
algorithms). | algorithms). | ||
Należy pamiętać że iterator nie wie na jaki kontener wskazuje, czyli | Należy pamiętać, że iterator nie wie na jaki kontener wskazuje, czyli | ||
poprzez iterator nie ma dostępu do interfejsu kontenera. | poprzez iterator nie ma dostępu do interfejsu kontenera. | ||
Iteratory pozwalają określanie zakresu elementów w kontenerze poprzez | Iteratory pozwalają na określanie zakresu elementów w kontenerze poprzez | ||
podanie iteratora wskazującego na początek i na pierwszy element poza | podanie iteratora wskazującego na początek i na pierwszy element poza | ||
końcem zakresu. Zakres oznaczamy poprzez [ | końcem zakresu. Zakres oznaczamy poprzez [<tt>it1</tt>,<tt>it2</tt>) (zob. [[#rys.2.4|rysunek 2.4]]). | ||
{{kotwica|rys.2.4|}}<center> | |||
[[File:cpp-2-zakres.svg|500x200px|thumb|center|Rysunek 2.4. Zakres.]] | |||
</center> | |||
Z tego powodu dozwolona jest instrukcja | Z tego powodu dozwolona jest instrukcja | ||
pobrania adresu pierwszego elementu poza końcem tablicy. | pobrania adresu pierwszego elementu poza końcem tablicy. | ||
<Source> | |||
double x[10]; | double x[10]; | ||
double *end | double *end=&x[10]; | ||
/ | //zwykłe wskażniki mogą być użyte jako iteratory | ||
std::cout<<accumulate(x,end,0)<<endl; | std::cout<<accumulate(x,end,0)<<endl; <i>suma elementów tablicy</i> | ||
</Source> | |||
Każdy kontener posiada motody | Każdy kontener posiada motody <tt>begin()</tt> i <tt>end()</tt>, zwracające | ||
iterator na początek i "poza koniec". Typowa pętla obsługi kontenera | iterator na początek i "poza koniec". Typowa pętla obsługi kontenera | ||
wygląda więc następująco: | |||
<Source> | |||
typedef vector<int>::iterator iterator; | typedef vector<int>::iterator iterator; | ||
vector<it> v(100); | vector<it> v(100); | ||
for(iterator it | for(iterator it=v.begin();it!=v.end();++it) { | ||
... | ... | ||
} | } | ||
/ | </Source> | ||
Proszę zwrócić uwagę na wykorzystanie | ([[media:Accumulate.cpp | Źródło: accumulate.cpp]]) | ||
Proszę zwrócić uwagę na wykorzystanie operatora <tt><nowiki>!=</nowiki></tt> do sprawdzenia | |||
końca zakresu. Tylko iteratory o dostępie swobodnym mogą być | końca zakresu. Tylko iteratory o dostępie swobodnym mogą być | ||
porównywane za pomocą operatora | porównywane za pomocą operatora <tt>operator<()</tt>. Reszta jest tylko | ||
<tt>EqualityComparable</tt>. | |||
===Algorytmy=== | ===Algorytmy=== | ||
Linia 704: | Linia 666: | ||
dostęp do różnych kontenerów. Takie podejście ma też inne | dostęp do różnych kontenerów. Takie podejście ma też inne | ||
konsekwencje, jak już pisałem iterator nie wie z jakiego kontenera | konsekwencje, jak już pisałem iterator nie wie z jakiego kontenera | ||
pochodzi, w szczególności oznacza to że | pochodzi, w szczególności oznacza to, że algorytmy ogólne nie mogą | ||
usuwać elementów z kontenera. | usuwać elementów z kontenera. | ||
Oczywiście część algorytmów np. | Oczywiście część algorytmów, np. | ||
<tt>sort</tt>, wymaga bardziej wyrafinowanych iteratorów, nie dostarczanych | |||
przez każdy kontener. Wiele jednak jednoprzebiegowych algorytmów | przez każdy kontener. Wiele jednak jednoprzebiegowych algorytmów | ||
zadawala się iteratorami wejściowymi. | zadawala się iteratorami wejściowymi. | ||
Linia 714: | Linia 676: | ||
Poza iteratorami uogólnione algorytmy wykorzystują obiekty funkcyjne | Poza iteratorami uogólnione algorytmy wykorzystują obiekty funkcyjne | ||
czyli funktory. Obiekt funkcyjny to koncept będący uogólnieniem | czyli funktory. Obiekt funkcyjny to koncept będący uogólnieniem | ||
pojęcia fukcji, czyli coś | pojęcia fukcji, czyli coś do czego można zastosować składnię | ||
wywołania funkcji. W C++ mogą to być funkcje, wskaźniki do funkcji | wywołania funkcji. W C++ mogą to być funkcje, wskaźniki do funkcji | ||
oraz obiekty klas w których zdefiniowano | oraz obiekty klas, w których zdefiniowano <tt>operator()(...)</tt> . | ||
Funktory w STL są podzielone ze względu na | Funktory w STL są podzielone ze względu na liczbę argumentów wywołania. | ||
<tt>Generator</tt> nie przyjmuje żadnego argumentu, <tt>UnaryFunction</tt> | |||
posiada jeden argument a | posiada jeden argument, a <tt>BinaryFunction</tt> - dwa argumenty wywołania. | ||
Ważną podklasą są funkcje zwracające wartość typu | Ważną podklasą są funkcje zwracające wartość typu <tt>bool</tt>, nazywane | ||
predykatami. Rozróżniamy więc | predykatami. Rozróżniamy więc <tt>UnaryPredicate</tt> i | ||
<tt>BinaryPredicate</tt>. | |||
Żeby zilustrować użycie algorytmów i funktorów rozważmy następujący | Żeby zilustrować użycie algorytmów i funktorów rozważmy następujący | ||
przykład. Najpierw definiujemy funktor który posłuży nam do | przykład. Najpierw definiujemy funktor, który posłuży nam do | ||
generowania sekwencji obiektów: | generowania sekwencji obiektów: | ||
template<typename T> class SequenceGen { | template<typename T> class SequenceGen { | ||
private: | private: | ||
T _start; | T _start; | ||
T _step; | T _step; | ||
public: | public: | ||
SequenceGen(T start <nowiki>=</nowiki> T(),T step <nowiki>=</nowiki> 1 ): | SequenceGen(T start <nowiki>=</nowiki> T(),T step <nowiki>=</nowiki> 1 ): | ||
_start(start),_step(step){}; | _start(start),_step(step){};<br> | ||
T operator()() {T tmp<nowiki>=</nowiki>_start; _start+<nowiki>=</nowiki>_step; return tmp;} | |||
}; | |||
([[media:Bind.cpp | Źródło: bind.cpp]]) | |||
Za pomocą obiektu klasy | Za pomocą obiektu klasy <tt>SequenceGen</tt> możemy wypełnić wektor | ||
sekwencją 20 pierwszych nieparzystych liczb całkowitych: | sekwencją 20 pierwszych nieparzystych liczb całkowitych: | ||
const size_t n <nowiki>=</nowiki> 20 ; | const size_t n <nowiki>=</nowiki> 20 ; | ||
vector<int> v(n); | vector<int> v(n); | ||
generate_n(v.begin(),n,SequenceGen<int>(1,2)); | generate_n(v.begin(),n,SequenceGen<int>(1,2)); | ||
([[media:Bind.cpp | Źródło: bind.cpp]]) | |||
Standardowy algorytm | Standardowy algorytm | ||
template <class OutputIterator, class Size, class Generator> | template <class OutputIterator, class Size, class Generator> | ||
OutputIterator generate_n(OutputIterator first, | OutputIterator generate_n(OutputIterator first, | ||
Size n, Generator gen); | Size n, Generator gen); | ||
służy właśnie do wypełniania kontenerów za pomocą | służy właśnie do wypełniania kontenerów za pomocą <tt>n</tt> kolejnych | ||
wyników wywołania funktora | wyników wywołania funktora <tt>gen</tt>. Powyższy kod ilustruje typowy | ||
sposób opisu algorytmów w STL. Nazwy parametrów szablonu odpowiadają | sposób opisu algorytmów w STL. Nazwy parametrów szablonu odpowiadają | ||
nazwom konceptów które muszą modelować. | nazwom konceptów, które muszą modelować. | ||
W tak wypełnionym kontenerze | W tak wypełnionym kontenerze | ||
Linia 764: | Linia 727: | ||
pięć). Służy do tego algorytm | pięć). Służy do tego algorytm | ||
template<class InputIterator, class Predicate> | template<class InputIterator, class Predicate> | ||
InputIterator find_if(InputIterator first, | InputIterator find_if(InputIterator first, | ||
InputIterator last, | InputIterator last, | ||
Predicate pred); | Predicate pred); | ||
Który przeszukuje zakres | Który przeszukuje zakres <tt>[first,last)</tt> do napotkania pierwszego | ||
elementu dla którego predykat | elementu, dla którego predykat <tt>pred</tt> jest prawdziwy i zwraca | ||
iterator do tego elementu. Jeśli takiego elementu nie ma to | iterator do tego elementu. Jeśli takiego elementu nie ma, to | ||
<tt>find_if</tt> zwraca <tt>last</tt>. Do zakończenia programu potrzebujemy | |||
jeszcze predykatu który testuje czy dana wartość jest większa od | jeszcze predykatu, który testuje czy dana wartość jest większa od | ||
czterech. Zamiast go implementować skorzystamy z | czterech. Zamiast go implementować skorzystamy z adaptera funkcji | ||
<tt>bind2nd</tt>. Ta funkcja przyjmuje funktor dwuargumentowy | |||
( | (<tt>AdaptableBinaryFunction</tt>) <tt>F(T,U)</tt> i jakąś wartość <tt>x</tt> typu | ||
<tt>U</tt> i zwraca funktor jednoparametrowy <tt>F(T,x)</tt>. Korzystając z | |||
predefiniowanego predykatu | predefiniowanego predykatu <tt>greater</tt> możemy napisać: | ||
vector<int>::iterator it<nowiki>=</nowiki> find_if(v.begin(),v.end(), | vector<int>::iterator it<nowiki>=</nowiki> find_if(v.begin(),v.end(), | ||
bind2nd(greater<int>(),4)); | bind2nd(greater<int>(),4)); | ||
if(it!<nowiki>=</nowiki>v.end()) | if(it!<nowiki>=</nowiki>v.end()) | ||
cout<<*it<<endl; | cout<<*it<<endl; | ||
else | else | ||
cout<<"nie znaleziono zadanego | cout<<"nie znaleziono zadanego elementu"; | ||
} | } | ||
([[media:Bind.cpp | Źródło: bind.cpp]]) | |||
STL wprowadza więc do C++ elementy | STL wprowadza więc do C++ elementy programowania funkcyjnego. | ||
==Debugowanie== | ==Debugowanie== | ||
Linia 796: | Linia 760: | ||
Programowanie uogólnione korzysta istotnie z pojęcia konceptu. Koncept | Programowanie uogólnione korzysta istotnie z pojęcia konceptu. Koncept | ||
opisuje abstrakcyjne typy danych (czy funkcji) które mogą być użyte | opisuje abstrakcyjne typy danych (czy funkcji), które mogą być użyte | ||
jako argumenty danego szablonu. Definiowanie konceptu polega tylko na | jako argumenty danego szablonu. Definiowanie konceptu polega tylko na | ||
jego opisie. C++ nie posiada żadnego | jego opisie. C++ nie posiada żadnego mechanizmu pozwalającego na | ||
bardziej formalną definicję. Co za tym idzie nie można | bardziej formalną definicję. Co za tym idzie, nie można | ||
też automatycznie sprawdzać czy nasz typ modeluje żądany koncept. | też automatycznie sprawdzać czy nasz typ modeluje żądany koncept. | ||
Oczywiście kompilator podczas konkretyzacji szablonu sprawdza | Oczywiście kompilator podczas konkretyzacji szablonu sprawdza | ||
syntaktyczną zgodność przekazanego typu z wymaganiami szablonu. Nie | syntaktyczną zgodność przekazanego typu z wymaganiami szablonu. Nie | ||
jest to jednak idealne narzędzie diagnostyczne. Po pierwsze komunikat | jest to jednak idealne narzędzie diagnostyczne. Po pierwsze, komunikat | ||
o błedzie może być bardzo zawiły i na pewno nie będzie się odnosił do | o błedzie może być bardzo zawiły i na pewno nie będzie się odnosił do | ||
nazwy konceptu. Po drugie może się okazać że szablon który | nazwy konceptu. Po drugie, może się okazać, że szablon, który | ||
konkretyzujemy nie wykorzystuje wszystkich możliwych wyrażeń konceptu. | konkretyzujemy nie wykorzystuje wszystkich możliwych wyrażeń konceptu. | ||
Zresztą idea konceptu polega na rozdzieleniu definicji abstrakcyjnego | Zresztą idea konceptu polega na rozdzieleniu definicji abstrakcyjnego | ||
typu od definicji szablonu którego ten typ może być argumentem. | typu od definicji szablonu, którego ten typ może być argumentem. | ||
Rozwiazaniem jest napisanie własnego zestawu szablonów, których | Rozwiazaniem jest napisanie własnego zestawu szablonów, których | ||
jedynem zadaniem jest sprawdzanie zgodności przekazanych argumentów | jedynem zadaniem jest sprawdzanie zgodności przekazanych argumentów | ||
szablonu definiowanym przez ten szablon konceptem. Niestety można w | szablonu z definiowanym przez ten szablon konceptem. Niestety, można w | ||
ten sposób sprawdzać tylko zgodność syntaktyczną. | ten sposób sprawdzać tylko zgodność syntaktyczną. | ||
Idea tworzenia takich szablonów jest prosta (zob. | Idea tworzenia takich szablonów jest prosta (zob. [http://www.boost.org/libs/concept_check/concept_check.htm http://www.boost.org/libs/concept_check/concept_check.htm]): dla każdego | ||
konceptu tworzymy szablon zawierający funkcję <tt>constraints()</tt>, która | |||
konceptu tworzymy szablon zawierający funkcję | |||
zawiera wszystkie możliwe poprawne wyrażenia dla danego konceptu. Np. | zawiera wszystkie możliwe poprawne wyrażenia dla danego konceptu. Np. | ||
dla konceptu | dla konceptu <tt>Comparable</tt> możemy zdefiniować: | ||
template<typename T> struct ComparableConcept { | template<typename T> struct ComparableConcept { | ||
void constraints() { | void constraints() { | ||
require_boolean_expr( a > b); | require_boolean_expr( a > b); | ||
require_boolean_expr( a < b); | require_boolean_expr( a < b); | ||
}; | }; | ||
T a,b; | T a,b; | ||
}; | }; | ||
([[media:Concept_check.cpp | Źródło: concept_check.cpp]]) | |||
Szablon <tt>require_boolean_expr</tt> | |||
template <class TT> | |||
void require_boolean_expr(const TT& t) { | template <class TT> | ||
bool x = t; | void require_boolean_expr(const TT& t) { | ||
ignore_unused_variable_warning(x); | bool x = t; | ||
ignore_unused_variable_warning(x); | |||
} | <i>używa zmiennej <tt>x</tt> aby kompilator nie generował ostrzeżenia</i> | ||
} | |||
([[media:Concept_check.cpp | Źródło: concept_check.cpp]]) | |||
sprawdza czy jego argument, a więc wartość zwracana przez operatory, | sprawdza czy jego argument, a więc wartość zwracana przez operatory, | ||
może być konwertowana na | może być konwertowana na <tt>bool</tt>. | ||
Zwracam uwagę że nie możemy w kodzie | Zwracam uwagę, że nie możemy w kodzie szablonu <tt>Comparable</tt> użyć | ||
defaultowego konstruktora, bo nie jest on wymagany. Dlatego zmienne | defaultowego konstruktora, bo nie jest on wymagany. Dlatego zmienne | ||
<tt>a</tt> i <tt>b</tt> nie były zdefiniowane wewnątrz funkcji | |||
<tt>constraints()</tt>, tylko jako pola składowe klasy. Ponieważ nie | |||
tworzymy żadnej instancji tej klasy to nie będą wywoływane | tworzymy żadnej instancji tej klasy, to nie będą wywoływane | ||
konstruktory, a więc kompilator nie będzie generował ich kodu. | konstruktory, a więc kompilator nie będzie generował ich kodu. | ||
Teraz potrzebujemy jeszcze sposobu aby skompilować ale nie | Teraz potrzebujemy jeszcze sposobu, aby skompilować, ale nie wywołać, | ||
funkcję | funkcję <tt>ComparableConcept<T>::constraints()</tt>. Możemy | ||
tego dokonać pobierając adres funkcji i przypisując go wskaźnika. | tego dokonać pobierając adres funkcji i przypisując go do wskaźnika. | ||
Kompilator skompiluje kod funkcji, ale jej nie wykona. Dodatkowo | Kompilator skompiluje kod funkcji, ale jej nie wykona. Dodatkowo | ||
najprawdopodobniej kompilator optymalizujący usunie to przypisanie | najprawdopodobniej kompilator optymalizujący usunie to przypisanie | ||
jako | jako nieużywany kod, ale dopiero po kompilacji (no chyba, że jest | ||
bardzo ale to bardzo inteligentny). Dla wygody opakujemy | bardzo, ale to bardzo inteligentny). Dla wygody opakujemy tę | ||
konstrukcję w szablon funkcji: | konstrukcję w szablon funkcji: | ||
Możemy teraz używać | template <class Concept> | ||
inline void function_requires() { | |||
main() { | void (Concept::*x)() = &Concept::constraints; | ||
function_requires<ComparableConcept<int> >(); | ignore_unused_variable_warning(x); | ||
function_requires<ComparableConcept<std::complex> >();/ | } | ||
} | |||
([[media:Concept_check.cpp | Źródło: concept_check.cpp]]) | |||
Możemy teraz używać szablonu <tt>Comparable</tt> w następujący sposób: | |||
main() { | |||
function_requires<ComparableConcept<int> >(); | |||
function_requires<ComparableConcept<std::complex> >(); <i>błąd</i> | |||
} | |||
([[media:Concept_check.cpp | Źródło: concept_check.cpp]]) | |||
Bardziej skomplikowane koncepty możemy sprawdzać korzystając z | Bardziej skomplikowane koncepty możemy sprawdzać korzystając z | ||
klas sprawdzających dla innych konceptów np: | klas sprawdzających dla innych konceptów, np: | ||
void constraints() { | template <class Container> | ||
struct Mutable_ContainerConcept | |||
function_requires< ContainerConcept<Container> >(); | { | ||
function_requires< AssignableConcept<value_type> >(); | typedef typename Container::value_type value_type; | ||
function_requires< InputIteratorConcept<iterator> >(); | typedef typename Container::reference reference; | ||
typedef typename Container::iterator iterator; | |||
i = c.begin(); | typedef typename Container::pointer pointer;<br> | ||
i = c.end(); | void constraints() { | ||
c.swap(c2); | <i>sprawdzamy czy spełnia wymagania konceptu <tt>Container</tt></i> | ||
} | function_requires< ContainerConcept<Container> >(); | ||
iterator i; | function_requires< AssignableConcept<value_type> >(); | ||
Container c, c2; | function_requires< InputIteratorConcept<iterator> >();<br> | ||
}; | i = c.begin(); | ||
i = c.end(); | |||
c.swap(c2); | |||
} | |||
iterator i; | |||
Container c, c2; | |||
}; | |||
Biblioteka | Biblioteka <tt>boost</tt>, skąd wzięty został ten przykład, posiada | ||
implementację szablonów dla każdego konceptu z | implementację szablonów dla każdego konceptu z | ||
STL | STL ([http://www.boost.org/libs/concept_check/concept_check.htm http://www.boost.org/libs/concept_check/concept_check.htm]). Hierachia, którą | ||
można tam odczytać różni się trochę od tej którą wcześniej | można tam odczytać, różni się trochę od tej, którą wcześniej | ||
zaprezentowałem i która jest opisana w | zaprezentowałem i która jest opisana w [http://www.sgi.com/tech/stl/ http://www.sgi.com/tech/stl/]. Główna | ||
różnica to wprowadzenie rozróżnienia pomiędzy kontenerami które | różnica to wprowadzenie rozróżnienia pomiędzy kontenerami, które | ||
umożliwiaja modyfikację swoich elementów ( | umożliwiaja modyfikację swoich elementów (<tt>MutableContainer</tt>) i tych, | ||
które na to nie pozwalają ( | które na to nie pozwalają (<tt>Container</tt>). | ||
===Archeotypy=== | ===Archeotypy=== | ||
Linia 916: | Linia 879: | ||
odwrotną: implementujemy jakiś algorytm ogólny i chcemy się dowiedzieć | odwrotną: implementujemy jakiś algorytm ogólny i chcemy się dowiedzieć | ||
jaki koncept jest wymagany dla parametrów szablonu? Chcemy wybrać jak | jaki koncept jest wymagany dla parametrów szablonu? Chcemy wybrać jak | ||
najogólniejszy koncept który jeszcze pozwala na poprawne działanie | najogólniejszy koncept, który jeszcze pozwala na poprawne działanie | ||
algorytmu. Pomóc mogą nam w tym archeotypy. Są to klasy które | algorytmu. Pomóc mogą nam w tym archeotypy. Są to klasy, które | ||
dokładnie implementują interfejs danego konceptu. | dokładnie implementują interfejs danego konceptu. Opierając się na | ||
[http://www.boost.org/libs/concept_check/concept_check.htm http://www.boost.org/libs/concept_check/concept_check.htm], przedstawię teraz | |||
implementację archeotypu dla konceptu | implementację archeotypu dla konceptu <tt>Comparable</tt>. | ||
konstruktora kopiujacego oraz operatora przypisania dlatego w naszym | Koncept | ||
<tt>Comparable</tt> nie wymaga posiadania konstruktora defaultowego, | |||
konstruktora kopiujacego oraz operatora przypisania, dlatego w naszym | |||
archeotypie zdefiniujemy je jako prywatne: | archeotypie zdefiniujemy je jako prywatne: | ||
class comparable_archetype { | class comparable_archetype { | ||
private: | private: | ||
comparable_archetype() {}; | comparable_archetype() {}; | ||
comparable_archetype(const comparable_archetype &) {}; | comparable_archetype(const comparable_archetype &) {}; | ||
comparable_archetype &operator<nowiki>=</nowiki>(const comparable_archetype &) { | comparable_archetype &operator<nowiki>=</nowiki>(const comparable_archetype &) { | ||
return *this;}; | return *this;}; | ||
public: | public: | ||
comparable_archetype(dummy_argument) {}; | comparable_archetype(dummy_argument) {}; | ||
}; | }; | ||
Aby móc tworzyć obiekty typu | ([[media:Archeotype.cpp | Źródło: archeotype.cpp]]) | ||
Aby móc tworzyć obiekty typu <tt>comparable_archetype</tt> dodaliśmy | |||
niestandardowy konstruktor z argumentem sztucznego typu: | niestandardowy konstruktor z argumentem sztucznego typu: | ||
class dummy_argument {}; | class dummy_argument {}; | ||
używanego tylko na | używanego tylko na tę okazję (jego nazwa powinna być unikatowa). | ||
Operator | Operator <tt>operator<()</tt> nie musi zwracać wartości typu <tt>bool</tt>, a | ||
jedynie wartość typu | jedynie wartość typu konwertowalnego na <tt>bool</tt>, dlatego tworzymy taki typ: | ||
struct boolean_archetype { | struct boolean_archetype { | ||
operator const bool() const {return true;} | operator const bool() const {return true;} | ||
}; | }; | ||
i podajemy go jako typ zwracany przez operatory porównania | i podajemy go jako typ zwracany przez operatory porównania | ||
boolean_archetype operator<(const comparable_archetype &, | boolean_archetype operator<(const comparable_archetype &, | ||
const comparable_archetype &){ | |||
return boolean_archetype(); | return boolean_archetype(); | ||
}; | }; | ||
boolean_archetype operator>(const comparable_archetype &, | boolean_archetype operator>(const comparable_archetype &, | ||
const comparable_archetype &){ | |||
return boolean_archetype(); | return boolean_archetype(); | ||
}; | }; | ||
([[media:Archeotype.cpp | Źródło: archeotype.cpp]]) | |||
Teraz możemy już przetestować nasz szablon <tt>max</tt>. | |||
template<typename T> | |||
const T &max(const T &a,const T &b) {return (a>b)?a:b;}<br> | |||
main() { | |||
comparable_archetype ca(dummy_argument()); | |||
max(ca,ca); | |||
} | |||
([[media:Archeotype.cpp | Źródło: archeotype.cpp]]) | |||
Poprawna kompilacja tego kodu przekonuje nas, że koncept | |||
<tt>Comparable</tt> jest wystarczajacy, przynajmniej syntaktycznie. Proszę | |||
zwrócić uwagę, że jeśli użyjemy orginalnego szablonu | |||
/ | |||
template<typename T> T max(T a,T b) {return (a>b)?a:b;} | |||
{ | |||
([[media:Archeotype.cpp | Źródło: archeotype.cpp]]) | |||
to kod się nie skompiluje bo zabraknie konstruktora kopiujacego. | to kod się nie skompiluje, bo zabraknie konstruktora kopiujacego. | ||
Większość konceptów jest uszczegółowieniem innych konceptów. | Większość konceptów jest uszczegółowieniem innych konceptów. | ||
Implementacja archeotypów w biblitece | Implementacja archeotypów w biblitece [http://www.boost.org/libs/concept_check/concept_check.htm boost] zezwala na takie | ||
konstrukcje i gorąco zachęcam do zapoznania się z nią. | |||
konstrukcje i gorąco zachęcam do zapoznania się z | |||
Aktualna wersja na dzień 11:03, 5 wrz 2023
Wprowadzenie
W poprzednim wykładzie wprowadziłem pojęcia szablonów funkcji i klas. Są to bardzo ważne konstrukcje języka C++ dające programistom bezpośrednie, czyli z poziomu języka, wsparcie dla tworzenia uogólnionych funkcji i typów (nazywanych też funkcjami lub typami parametryzowanymi). Uogólnienie polega na tym, że za jednym zamachem definiujemy całe rodziny klas lub funkcji. Po podstawieniu za parametry konkretnych argumentów szablonu dostajemy już egzemplarz "zwykłego" typu (klasy) lub funkcji (nazywane również instancjami szablonu). Argumenty szablonu mogą reprezentować typy i w ten sposób dostajemy narzędzie umożliwiające pisanie ogólnego kodu parametryzowanego typem używanych w nim zmiennych, typem argumentów wywołania funkcji itp.
Szablony okazały się bardzo silnym narzędziem, których zastosowanie daleko przekracza implementację prostych kontenerów i można spokojnie stwierdzić, że ich prawdziwy potencjał jest ciągle odkrywany. Szablony idealnie wspierają styl programowania nazywany programowaniem uogólnionym. Polega on na generalizowaniu algorytmów i struktur danych tak, aby były w dużej mierze niezależne od typów danych, na których działają lub z których się składają. Mam nadzieję, że po lekturze poprzedniego wykładu Państwo już widzą, że to jest właśnie to, do czego szablony zostały wymyślone. Nie oznacza to, że automatycznie każdy program używajacy szablonów jest od razu programem uogólnionym. Tak jak i w przypadku tworzenia zwykłych (bez szablonów) programów, trzeba się sporo natrudzić, aby uzyskać uniwersalny, łatwy do ponownego wykorzystania kod. Ten wykład ma właśnie za zadanie przekazać Państwu podstawowe wiadomości na temat pisania dobrych programów uogólnionych.
W programowaniu uogólnionym ważną rolę gra pojęcie konceptu. Koncept to asbtrakcyjna definicja rodziny typów. To pojęcie pełni podobną rolę jak interfejs w programowaniu uogólnionym, ale przynależność do tej rodziny jest określona proceduralnie: do konceptu należą typy, które spełniają pewne wymagania. Czyli jeśli coś kwacze jak kaczka to jest to kaczka, a nie: to jest kaczka jeśli należy do rodziny "kaczowatych". Koncepty omówię w dalszej części tego wykładu.
Co to jest programowanie uogólnione łatwiej jest pokazać na przykładach niż opisać. Niewątpliwie najważniejszą i najbardziej znaną aplikacją programowania ogólnego jest Standardowa Biblioteka Szablonów (STL - Standard Template Library), będąca oficjalną częścią standardu C++. W tych wykladach będę się bardzo często posługiwał przykładami z STL-a, ale szczegółowe nauczenie posługiwania się tą biblioteką nie jest celem tego wykładu. Powinni jednak Państwo zrobić to sami. Dlatego zachęcam do analizy przykładów zamieszczonych na wykładzie oraz wykonywanie podanych ćwiczeń.
Drugim znakomitycm źródłem przykladów uogólnionego kodu jest repozytorium bibliotek boost. Stamtąd też będę podawał przykłady i znów gorąco zachęcam Państwa do zaglądania tam samemu.
Programowanie uogólnione samo w sobie szczególnie obiektowe nie jest, choć oczywiście wymaga możliwości definiowania własnych typów. Oba style programowania: uogólniony i obiektowy można oczywiście stosować razem. Każdy ma swoje charakterystyczne cechy i aby je podkreślić jeszcze raz przypomnę podstawy programowania obiektowego rozumianego jako programowanie z użyciem interfejsów(klas abstrakcyjnych) i funkcji wirtulanych.
Polimorfizm dynamiczny
Sercem programowania obiektowego, oczywiście poza koncepcją klasy i obiektu, jest polimorfizm dynamiczny, czyli możliwość decydowania o tym jaka funkcja zostanie wywołana pod daną nazwą nie w momencie kompilacji (czyli pisania kodu), ale w samym momecie wywołania. Zilustrujemy to na przykładzie. W tym celu skorzystamy z "matki wszystkich przykładów programowania obiektowego", czyli klasy kształtów graficznych:).
Problem jest następujący: nasz program w pewnym momencie musi manipulować kształtami graficznym: rysować, przesuwać, obracać itp. Jest w miarę oczywiste, że każdy kształt będzie posiadał swoją klasę. Następnym krokiem jest ocena które operacje w naszym kodzie wymagają szczególowej znajomości kształtu, a które tylko ogólnych jego własności. Ewidentnie operacja rysowania obiektu należy do tych pierwszych i musi być zdefiniowana w klasie danego kształtu. Mówimy, że "obiekt wie jak się narysować". Często mówi się o tym również jako o ustaleniu odpowiedzialności, czy o podziale obowiązków. Tak więc ustaliliśmy, że do obowiązków obiektu należy umiejętność narysowania się. Jeśli tak, to właściwie cała część kodu manipulującego kształtami nie musi znać szczegółów ich implementacji. Weźmy na przykład fragment aplikacji odpowiedzialny za odświeżanie ekranu. Zakładamy, że wskaźniki do wyświetlanych kształtów są przechowywane w tablicy shape_table:
for(size_t i=0;i<n;++i) shape_table[i]->draw();
kod źródłowy
Programista piszący ten kod nie musi wiedziec jakiego typu kształt jest przechowywany w danym elemencie tablicy shape_table i jak jest zaimplementowana funkcja draw. Istotne jest by każdy obiekt, którego wkaźnik przechowywany jest w tej tablicy posiadał metodę draw. Innymi słowy programista korzysta tu tylko ze znajomości i dostępności interfejsu obiektów typu kształt, a resztę wykonuje kompilator, który generuje kod zapewniający wywołanie odpowiedniej funkcji. Aby taki interfejs zdefiniować tworzymy abstrakcyjną klasę obiektów typu kształt:
class Shape { protected: long int _x; long int _y; public: Shape(long x,long y):_x(x),_y(y){}; long get_x() const {return _x;} long get_y() const {return _y;} virtual void draw() = 0; virtual ~Shape() {}; };
Klasa ta stanowić będzie klasę bazową dla wszystkich klas opisujących kształty. Klasa Shape jest klasą abstrakcyjną, ponieważ zawiera niezaimplementowaną wirtualną czystą fukcję void draw(). Kod definiujący konkretne klasy kształtów może wyglądać następująco:
class Rectangle: public Shape { protected: long _ur_x; long _ur_y; public: Rectangle(long ll_x,long ll_y,long ur_x,long ur_y): Shape(ll_x,ll_y),_ur_x(ur_x-ll_x),_ur_y(ur_y-ll_y) {}; virtual void draw() { std::cerr<<"rectangle : "<<_x<<" "<<_y<<" : "; std::cerr<<_ur_x+_x<<" "<<_ur_y+_y<<std::endl; } long get_ur_x() const {return _ur_x;}; long get_ur_y() const {return _ur_y;}; };
i
class Circle: public Shape { protected: long _r; public: Circle(long x, long y,long r) :Shape(x,y), _r(r) {}
virtual void draw() { std::cerr<<"Circle : "<<_x<<" "<<_y<<" : "<<_r<<std::endl; } long get_r() const {return _r;}; };
Teraz możemy zdefiniować już funkcję odświeżającą ekran:
void draw_shapes(Shape *table[],size_t n) { for(size_t i=0;i<n;++i) table[i]->draw(); }
Funkcja draw_shapes wykorzystuje zachowanie polimorficzne: to która funkcja draw zostanie wywołana zależy od tego jaki konkretny kształt jest wskazywany przez element tablicy. Łatwo się o tym przekonać wykonując np. następujący kod
int main() { Shape *list[4]; list[0]=new Circle(0,0,100); list[1]=new Rectangle(20,20,80,80); list[2]=new Circle(10,10,100); list[3]=new Rectangle(20,0,80,10); draw_shapes(list,4); }
kod źródłowy
W ten sposób zaimplementowaliśmy podstawowy paradygmat programowania obiektowego: rozdzielenie interfejsu od implementacji za pomocą abstrakcyjnej klasy bazowej i wykorzystanie funkcji wirtualnych. Ważną częścią tego procesu jest więc właśnie odpowiedni wybór interfejsów (klas bazowych).
Polimorfizm statyczny
Patrząc na kod funkcji draw_shapes możemy zauważyć, że korzysta on jedynie z własności posiadania przez wskazywane obiekty metody draw(). To sygnatura, czyli typ parametru wywołania tej funkcji określa, że musi to być wskaźnik na typ Shape. Z poprzedniego wykładu pamiętamy, że możemy zrezygnować z wymuszania typu argumentu wywołania funkcji poprzez użycie szablonu funcji:
template<typename T> void draw_template(T table[],size_t n) { for(size_t i=0;i<n;++i) table[i].draw(); }
Taką funkcję możemy wywołać dla dowolnej tablicy, byle tylko przechowywany typ posiadał metodę draw. Mogą to być obiekty typów Circle i Rectangle (nie Shape, obiekty klasy Shape nie istnieją!), ale też inne zupełnie z nimi nie związane. Ilustruje to poniższy przykład:
class Drawable { public: void draw() {cerr<<"hello world!"<<endl;} };
int main() { Drawable table_d[1]={Drawable()}; Circle table_c[2]={Circle(10,10),Circle(0,50)};
draw_template(table_d,1); draw_template(table_c,2); }
kod źródłowy
Korzystając z szablonów uzyskaliśmy więc również pewien efekt zachowania polimorficznego. W przeciwieństwie do poprzedniego przykładu jest to polimorfizm statyczny: to kompilator zadecyduje na podstawie typu tablicy jaką funkcję draw wywołać. Oczywiście w rozważanym przypadku to podejście jest całkowicie nieadekwatne, mamy bowiem do czynienia z niejednorodną rodziną kształtów, a wybór konkretnych kształtów dokunuje się podczas wykonywania programu. Podając przykład z szablonami chciałem tylko podkreślić różnice pomiędzy tymi dwoma technikami. Przykłady kiedy to szablony okazują się lepszym rozwiązaniem zostały podane w poprzednim wykładzie.
Polimorfizm statyczny vs. dynamiczny
Jak już wspomniałem każdy styl posiada swoje cechy, które w zależności od okoliczności mogą być postrzegane jako wady lub zalety. Poniżej podaję zebrane głowne właściwości każdego podejścia.
- Dziedzieczenie i funkcje wirtualne
- umożliwia pracę ze zbiorami niejednorodnych obiektów i korzysta z polimorfizmu dynamicznego
- wymaga wspólnej hierarchii dziedziczenia
- wymusza korzystanie ze wskaźników lub referencji i funkcji wirtualnych
- zazwyczaj generuje mniejszy kod.
- Szablony
- implementuje polimorfizm statyczny
- bezpiecznie obsługuje jednorodne zbiory obiektów
- nie trzeba korzystać ze wskaźników i referencji ani funkcji wirtualnych
- nie musimy korzystać ze wspólnej hierarchii dziedziczenia.
Koncepty
Przyjrzyjmy się jeszcze raz deklaracji funkcji draw_shapes i draw_template. Kiedy programista widzi deklarację:
void draw_shapes(Shape *table[])
wie, że interfejs wykorzystywany przez funkcję draw jest zdefiniowany przez klasę Shape. Aby go poznać musi przeczytać kod i dokumentację tej klasy. Natomiast kiedy programista widzi deklarację:
template<typename T> void draw_template(T table[],size_t n);
to musi prześledzić kod funkcji draw_templates aby poznać ograniczenia nałożone na argument szablonu T. W tym przypadku nie jest to trudne, ale ogólnie może to być nietrywialne zadanie.
Zamiast jednak definiować ograniczenia i warunki dla każdego szablonu osobno, możemy szukać wspólnych, powtarzających się zestawów warunków. Taki zestaw nazwiemy konceptem i będziemy go traktować jako abstrakcyjną definicję całej rodziny typów, niezależną od konkretnego szablonu. Typ spełniający warunki konceptu nazywamy modelem konceptu lub mówimy, że modeluje ten koncept. Mając wybrany, dobrze przygotowany zestaw konceptów dla danej dziedziny, możemy się nimi posługiwać przy definiowaniu typów i algorytmów uogólnionych.
Koncepty mogą tworzyć hierachie analogiczne do hierarachii dziedziecznia. Mówimy, że koncept A jest bardziej wyspecjalizowany niż B (A is-refinement-of B), jeśli zestaw ograniczeń konceptu B zawiera się w zestwie ograniczeń konceptu A. Będę też używał określenia A jest "uszczegółowieniem" B.
Pojęcie konceptu pełni więc przy programowaniu za pomocą szablonów podobną rolę jak pojęcie interfejsu przy programowaniu za pomocą abstrakcyjnych klas bazowych i polimorfizmu dynamicznego. W przeciwieństwie do interfejsu jest to jednak pojęcie bardziej "ulotne", bo nie narzucamy go za pomocą formalnej definicji klasy abstrakcyjnej. Koncepty definiujemy poprzez mniej lub bardziej ścisłe wypisanie nakładanych przez nie ograniczeń. Ograniczenia te mogą zawierać między innymi:
- Prawidłowe wyrażenia. Zestaw wyrażeń języka C++, które muszą się poprawnie kompilować.
- Typy stowarzyszone. Ewentualne dodatkowe typy występujące w prawidłowych wyrażeniach.
- Semantyka: zanczenie wyrażeń. Jednym ze sposobów określanie semantyki jest podawanie niezmienników, czyli wyrażeń, które dla danego konceptu są zawsze prawdziwe.
- Złożoność algorytmów. Gwarancje co do czasu i innych zasobów potrzebnych do wykonania danego wyrażenia.
Programowanie uogólnione polega więc na wyszukiwaniu konceptów na tyle ogólnych, aby pasowały do dużej liczby typów i na tyle szczegółowych, aby zezwalały na wydajną implementację.
Definiowanie konceptów
Weźmy za przykład szablon funkcji max z poprzedniego wykładu
template<typename T> max(T a,T b) {return (a>b)?a:b;}
i zastanówmy się, jakie koncepty możemy odkryć w tak prostym kodzie.
Zacznijmy od gramatyki. Jakie warunki musi spełniać typ T, aby podstawienie go jako argument szablonu max dawało poprawne wyrażenie? Oczywistym warunkiem jest, że dla tego typu musi być zdefiniowany operator porównania bool operator>(...). Specjalnie nie wyspecyfikowałem sygnatury tego operatora. Nie ma np. znaczenia jak parametry są przekazywane, co więcej operator>(...) może być zdefiniowany jako składowa klasy i posiadać tylko jeden jawny argument. Ważne jest to, że jeśli x i y są obiektami typu T to wyrażenie:
x>y
jest poprawne (skompiluje się).
Łatwiej jest przeoczyć fakt, że ponieważ argumenty wywołania są zwracane i przekazywane przez wartość, to typ T musi posiadać konstruktor kopiujący. Oznacza to, że jeśli x i y są obiektami typu T to wyrażenia:
T(x); T x(y); T x = y;
są poprawne.
Przykład 2.1
Spełnienie obydwu tych warunków zapewni nam poprawność gramatyczną wywołania szablonu z danym typem, tzn. kod się skompiluje.
A co z poprawnością semantyczną? Mogłoby sie wydawać, że jest bez znaczenia jak zdefiniujemy operator>(...). Koncept typu T jest jednak częścią kontraktu dla funkcji max. Kontraktu zawieranego pomiędzy twórcą tego wielce skomplikowanego kodu, a jego użytkownikiem. Kontrakt stanowi, że jeżeli użytkownik dostarczy do funkcji argumenty o typach zgodnych z konceptem i o wartościach spełniających być może inne warunki wstępne, to twórca funkcji gwarantuje, że zwróci ona poprawny wynik.
Zastanówny się więc jak zdefiniować poprawność dla funkcji maksimum. Z definicji maksimum żaden element argument funkcji max nie może być większy od wyniku, czyli wyrażenie
musi być zawsze prawdziwe. Jasne jest, że jeśli dla jakiegoś typu X zdefiniujemy operator porównania tak, aby zwracał zawsze prawdę
bool operator>(const X &a,const X &b) {return 1;}
lub aby był równoważny operatorowi równości:
bool operator>(const X &a,const X &b) {return a==b;}
to wyrażenie 2.1 nie może być prawdziwe dla żadnej wartości a i b. Aby funkcja max mogła spełnić swój warunek końcowy musimy narzucić pewne ograniczenia semantyczne na operator>(). Te warunki to żądanie, aby relacja większości definiowana przez ten operator byłą relacją porządku częściowego, a więc aby spełnione było
To rozumowanie możnaby ciągnąć dalej i zauważyć, że nawet z tym ograniczeniem uzyskamy nieintuicyjne wyniki w przypadku, gdy obiekty a i b będą nieporównywalne, tzn. i .
Poprawność semantyczną konstruktora kopiującego jest trudniej zdefiniować, ograniczymy się więc tylko do stwierdzenia, że wykonanie operacji 2.1 powoduje powstanie kopii obiektu x (cokolwiek by to nie znaczyło).
Comparable i Assignable
Reasumując, dostajemy zbiór warunków, które musi spełniać typ T, aby móc go podstawić do szablonu funkcji max. Czy to oznacza, że zdefiniowaliśmy już poprawny koncept? Żeby się o tym przekonać spróbujmy go nazwać. Narzuca się nazwa w stylu Comparable, ale wtedy łatwo zauważyć, że istnienie konstruktora kopiującego nie ma z tym nic wspólnego. Próbujemy upchnąc dwa niezależne pojęcia do jednego worka. Co więcej bardzo łatwo jest zrezygnować z konieczności posiadania konstruktora kopiujacego, zmieniając deklarację max na:
template<typename T> const T& max(const T&,const T&);
Teraz argumenty i wartość zwracana przekazywane są przez referencję i nie ma potrzeby kopiowania obiektów.
Logiczne jest więc wydzielenie dwu konceptów: jednego definiującego typy porównywalne, drugiego - typy "kopiowalne". Dalej możemy zauważyć, że istnienie operatora > automatycznie pozwala na zdefiniowanie operatora < poprzez:
bool operator<(const T& a,const T&b) {return b>a;};
Podobnie istnienie konstruktora kopiującego jest blisko związane z istnieniem operatora przypisania.
Tak więc dochodzimy do dwu konceptów: Comparable reprezentującego typy, których obiekty można porównywać za pomocą operatorów < i > oraz Assignable reprezentujacego typy, których obiekty możemy kopiować i przypisywać do siebie. Taką zabawę można kontynuować, pytając np. co z operatorem porównania operator==()?, co z konstruktorem defaultowym? itd. Widać więc, że koncepty to sprawa subietywna, ale to żadna nowość. Wybór używanych abstrakcji jest zawsze sprawą mniej lub bardziej subiektywną i silnie zależną od rozpatrywanego problemu. O tym czy dwa pojęcia włączymy do jednego konceptu czy nie decyduje np. odpowiedź na pytanie czy prawdopodobne jest użycie kiedykolwiek któregoś z tych pojęć osobno?
Tak więc zanim zaczniemy defniować koncepty musimy ustalić w jakim kontekście je rozpatrujemy. Na tym wykladzie kontekstem jest STL i oba wprowadzone koncepty są wzorowane na koncetach z STL-a. Należy jednak nadmienić, że pojęcie konceptu nie pojawia się wprost w definicji stadardu C++. Najlepiej koncepty STL przedstawione są na stronach firmy SGI dokąd Państwa odsyłam.
STL
Standardowa Biblioteka Szablonów (STL) to doskonałe narzędzie programistyczne zawarte w standardzie C++. Stanowi ona również znakomity, niejako sztandarowy, przykład programowania uogólnionego. Na tę bibliotekę można patrzeć więc dwojako: jako rozszerzenie języka C++ o dodatkowe funkcje lub jako na zbiór konceptów stanowiących podstawę do projetowania programów uogólnionych. Ja chciałbym podkreślić tutaj ten drugi aspekt, podkreślając jednak, że dobre poznanie możliwości STL-a może bardzo ułatwić Państwu prace programistyczne.
Biblioteka składa się zasadniczo z dwu części: uogólnionych kontenerów i uogólnionych algorytmów. Trzecią cześcią, niejako sklejającą te dwie, są iteratory.
Kontenery to obiekty służące do przechowywania innych obiektów. Kontenery w STL są jednorodne, tzn. mogą przechowywać tylko zbiory (kolekcje) obiektów tego samego typu. Kluczem do efektywnego programowania uogólnionego jest jednak sprawa ujednolicenia dostępu do zawartości kontenera. Rozważmy dla przykładu dwa typowe kontenery vector i list, implementujące odpowiednio "inteligentną" tablicę oraz listę dwukierunkową. Naturalnym sposobem dostępu do tablicy jest indeksowanie:
std::vector<int> v(10); v[8]=1;
a listy przeglądamy po kolei, przesuwając się o jeden element w przód czy w tył
Uwaga! To nie jest kod STL-owy !!! lista<int> l; l.reset(); ustawia element bieżacy na początek listy for(int i=0;i<8;i++) l.next(); przesuwa element bieżący o jeden element do przodu
l.current()=1; zwraca referencję do elementu bieżącego
Widać, że w takim sformułowaniu praktycznie nie jest możliwe napisanie ogólnego kodu np. dodającego wszystkie elementy kontenera czy wyszukującego jakiś element w kontenerze. Ponadto opisany sposób dostępu do listy ogranicza nas do korzystania z jednego bieżącego elementu na raz.
Rozwiązaniem tego problemu zastosowanym w STL jest koncept iteratora, który definiuje abstrakcyjny interfejs dostępu do elementów kontenera. W STL iterator posiada semantykę wskaźnika, w szczególności może być zwykłym wskaźnikiem, choć normalnie jest to wskaźnik inteligentny. Każdy kontener posiada zestaw funkcji zwracających iteratory do swojego początku i na swój koniec. Korzystając z nich można listę przeglądać następująco
std::list<int> l; tu jakoś inicjalizujemy liste for(list<int>::iterator it=l.begin();it!=l.end();it++) { każdy kontener definiuje typ stowarzyszony nazwany iterator cout<<*it<<endl; korzystamy z iteratorów jak ze zwykłych wskaźników } }
Przykładowy ogólny algorytm oparty o iteratory może wyglądać w ten sposób:
template <class InputIterator, class T> T accumulate(InputIterator first, InputIterator last, T init) { T total=init; for( ; first != last;++first) total+= *first; return total; }
Oczywiście nie da się zignorować fundamentalnych różnic pomiędzy listą a wektorem. Dlatego np. iterator wektora zezwala na konstrukcje it[i], a iterator listy już nie. Oznacza to, że algorytm, który działa dla iteratorów wektora (np. sort), nie musi działać dla iteratora listy. W języku konceptów oznacza to, że std::vector<T>::iterator jest modelem konceptu bardziej wyspecjalizowanego niż koncept, którego modelem jest std::list<T>::iterator. Zobaczymy to w następnej części tego wykładu.
Kontenery
Standard C++ definiuje dwa zestawy kontenerów wchodzące w skład STL:
- Sekwencje czyli pojemniki, w których kolejność elementów jest ustalana przez korzystającego z pojemnika (klienta) są to:
- Kontenery asocjacyjne, czyli pojemniki, w których klient nie ma kontroli nad kolejnością elementów, są to:
Ponadto różni dostawcy oferują dodatkowe pojemniki. Na uwagę zasługuje znakomita darmowa implementacja STL firmy Silicon Graphics, która miedy innymi wchodzi w skład pakietu g++ i dostarcza dodatkowo takich kontenerów jak: lista jednokierunkowa slist oraz tablice haszujące hash_set czy hash_map (zob. STL). Hierachię konceptów kontenerów typu sekwencji przedstawia rysunek 2.1, a kontenerów asocjacyjnych rysunek 2.2.


Nie będę tu omawiał tych wszystkich konceptów. Ich szczegółowe opisy znajdują się na stronie http://www.sgi.com/tech/stl/. Tutaj chciałbym tylko dodać parę luźnych komentarzy.
Po pierwsze, rodzi się pytanie czy taka skomplikowana taksonomia jest potrzebna? W końcu patrząc na rysunki widać, że konceptów jest dużo więcej niż typów kontenerów. Rzeczywiście, do posługiwania się biblioteką w zasadzie wystarczy zaznajomić się z opisami kontenerów i hierarchią iteratorów (zob. rysunek 2.3). Podane klasyfikacje przydają się dopiero kiedy dodajemy własne elementy do biblioteki. Dobierając do implemetacji najbardziej ogólny koncept spełniający nasze wymagania zwiększamy potencjał ponownego użycia naszego kodu z innymi komponentami biblioteki, czy kodem innych developerów.
Kontenery z STL są właścicielami swoich elementów, zniszczenie kontenera powoduje zniszczenie jego elementów. Wszytkie operacje wkładania elementów do kontenera używają przekazywania przez wartość, czyli kopiują wkładany obiekt. Jeżeli chcemy, aby czas życia elementów kontenera był dłuższy od czasu życia kontenera, należy użyć wskaźników.
Kontenery różnią się nie tylko rodzajem iteratorów, jaki implementują, ale również rodzajem operacji, które można wykonać bez unieważnienia istniejących iteratorów. Pokażę to na przykładzie:
std::vector<int>::iterator it; int i;
std::vector<int> v(1); std::vector<int> buff(100); staramy się zająć pamięć za v
v[0]=0; it=v.begin(); i=(*it); OK, przypisuje i=0 for(int i=0;i<10;++i) v.push_back(i); ponieważ przekraczamy koniec wektora, kontener zaalokuje dodatkową pamięć. Może się to wiązać z koniecznośćią przeniesienia zawartości wektora v w inne miejsce pamięci. To spowoduje, że wskaźnik it przestanie pokazywać na początek wektora v
std::cerr<<(*it)<<std::endl ; niezdefiniowane
std::cerr<<"iterator nieprawidlowy"<<std::endl; for(;it != v.end(); ++it) potencjalnie nieskończona pętla std::cerr<<*it<<std::endl; ;
std::cerr<<"iterator prawidlowy"<<std::endl; for(it=v.begin();it != v.end(); ++it) std::cerr<<*it<<std::endl; ;
Bardzo Państwa na ten problem uczulam. Efekt działania powyższego kodu jest gorzej niż zły: jest niezdefiniowany!, tzn. będzie zależał od implementacji kompilatora, od zadeklarownych wcześniej zmiennych itp. Proszę np. spróbować wykomentować linijkę
std::vector<int> buff(100); staramy się zająć pamięć za v
i porównać wynik działania programu. Może się również zdarzyć, że program zadziała poprawnie (wbrew pozorom jest to najgorsza możliwa sytuacja!).
Ważne są gwarancje złożoności metod kontenera. Ewidentnie każdy rodzaj kontenera może dostarczyć każdego rodzaju operacji, różny będzie jednak czas ich wykonywania. I tak rząd O(1) jest gwarantowany w operacji indeksowania wektora. Natomiast operacja dodania elementu w środku wektora jest rzędu O(N). Z listą jest odwrotnie i dlatego listy w STL nie posiadają operacji indeksowania.
Nie wszystkie własności kontenerów są zdefiniowane w konceptach. Każdy kontener może definiować dodatkowe metody właściwe tylko dla niego.
Iteratory
Iteratory to koncept, który uogólnia pojęcie wskaźnika. Hierarchię konceptów iteratorów przedstawia rysunek 2.3. Zaznaczono na nim również które koncepty kontenerów wymagają danego modelu iteratora.

Najprostsze iteratory pojawiające sie w STL-u to iteratory wejściowe i wyjściowe. Wprawdzie żaden kontener nie posiada iteratorów tego typu, ale iteratory wejściowe, umożliwiające tylko jednoprzebiegowe odczytanie wartości kontenera, są częstym wymaganiem dla argumentów algorytmów nie zmieniających elementów kontenera (non mutable algorithms).
Należy pamiętać, że iterator nie wie na jaki kontener wskazuje, czyli poprzez iterator nie ma dostępu do interfejsu kontenera.
Iteratory pozwalają na określanie zakresu elementów w kontenerze poprzez podanie iteratora wskazującego na początek i na pierwszy element poza końcem zakresu. Zakres oznaczamy poprzez [it1,it2) (zob. rysunek 2.4).

Z tego powodu dozwolona jest instrukcja pobrania adresu pierwszego elementu poza końcem tablicy.
double x[10];
double *end=&x[10];
//zwykłe wskażniki mogą być użyte jako iteratory
std::cout<<accumulate(x,end,0)<<endl; <i>suma elementów tablicy</i>
Każdy kontener posiada motody begin() i end(), zwracające iterator na początek i "poza koniec". Typowa pętla obsługi kontenera wygląda więc następująco:
typedef vector<int>::iterator iterator;
vector<it> v(100);
for(iterator it=v.begin();it!=v.end();++it) {
...
}
Proszę zwrócić uwagę na wykorzystanie operatora != do sprawdzenia końca zakresu. Tylko iteratory o dostępie swobodnym mogą być porównywane za pomocą operatora operator<(). Reszta jest tylko EqualityComparable.
Algorytmy
Algorytmy działają na zakresach elementów kontenera definiowanych przez dwa iteratory, a nie na kontenerach. Umożliwia to jednolity dostęp do różnych kontenerów. Takie podejście ma też inne konsekwencje, jak już pisałem iterator nie wie z jakiego kontenera pochodzi, w szczególności oznacza to, że algorytmy ogólne nie mogą usuwać elementów z kontenera.
Oczywiście część algorytmów, np. sort, wymaga bardziej wyrafinowanych iteratorów, nie dostarczanych przez każdy kontener. Wiele jednak jednoprzebiegowych algorytmów zadawala się iteratorami wejściowymi.
Poza iteratorami uogólnione algorytmy wykorzystują obiekty funkcyjne czyli funktory. Obiekt funkcyjny to koncept będący uogólnieniem pojęcia fukcji, czyli coś do czego można zastosować składnię wywołania funkcji. W C++ mogą to być funkcje, wskaźniki do funkcji oraz obiekty klas, w których zdefiniowano operator()(...) .
Funktory w STL są podzielone ze względu na liczbę argumentów wywołania. Generator nie przyjmuje żadnego argumentu, UnaryFunction posiada jeden argument, a BinaryFunction - dwa argumenty wywołania. Ważną podklasą są funkcje zwracające wartość typu bool, nazywane predykatami. Rozróżniamy więc UnaryPredicate i BinaryPredicate.
Żeby zilustrować użycie algorytmów i funktorów rozważmy następujący przykład. Najpierw definiujemy funktor, który posłuży nam do generowania sekwencji obiektów:
template<typename T> class SequenceGen { private: T _start; T _step; public: SequenceGen(T start = T(),T step = 1 ): _start(start),_step(step){};
T operator()() {T tmp=_start; _start+=_step; return tmp;} };
Za pomocą obiektu klasy SequenceGen możemy wypełnić wektor sekwencją 20 pierwszych nieparzystych liczb całkowitych:
const size_t n = 20 ; vector<int> v(n); generate_n(v.begin(),n,SequenceGen<int>(1,2));
Standardowy algorytm
template <class OutputIterator, class Size, class Generator> OutputIterator generate_n(OutputIterator first, Size n, Generator gen);
służy właśnie do wypełniania kontenerów za pomocą n kolejnych wyników wywołania funktora gen. Powyższy kod ilustruje typowy sposób opisu algorytmów w STL. Nazwy parametrów szablonu odpowiadają nazwom konceptów, które muszą modelować.
W tak wypełnionym kontenerze poszukamy pierwszego elementu większego od czterech (powinno to być pięć). Służy do tego algorytm
template<class InputIterator, class Predicate> InputIterator find_if(InputIterator first, InputIterator last, Predicate pred);
Który przeszukuje zakres [first,last) do napotkania pierwszego elementu, dla którego predykat pred jest prawdziwy i zwraca iterator do tego elementu. Jeśli takiego elementu nie ma, to find_if zwraca last. Do zakończenia programu potrzebujemy jeszcze predykatu, który testuje czy dana wartość jest większa od czterech. Zamiast go implementować skorzystamy z adaptera funkcji bind2nd. Ta funkcja przyjmuje funktor dwuargumentowy (AdaptableBinaryFunction) F(T,U) i jakąś wartość x typu U i zwraca funktor jednoparametrowy F(T,x). Korzystając z predefiniowanego predykatu greater możemy napisać:
vector<int>::iterator it= find_if(v.begin(),v.end(), bind2nd(greater<int>(),4)); if(it!=v.end()) cout<<*it<<endl; else cout<<"nie znaleziono zadanego elementu"; }
STL wprowadza więc do C++ elementy programowania funkcyjnego.
Debugowanie
Sprawdzanie konceptów
Programowanie uogólnione korzysta istotnie z pojęcia konceptu. Koncept opisuje abstrakcyjne typy danych (czy funkcji), które mogą być użyte jako argumenty danego szablonu. Definiowanie konceptu polega tylko na jego opisie. C++ nie posiada żadnego mechanizmu pozwalającego na bardziej formalną definicję. Co za tym idzie, nie można też automatycznie sprawdzać czy nasz typ modeluje żądany koncept.
Oczywiście kompilator podczas konkretyzacji szablonu sprawdza syntaktyczną zgodność przekazanego typu z wymaganiami szablonu. Nie jest to jednak idealne narzędzie diagnostyczne. Po pierwsze, komunikat o błedzie może być bardzo zawiły i na pewno nie będzie się odnosił do nazwy konceptu. Po drugie, może się okazać, że szablon, który konkretyzujemy nie wykorzystuje wszystkich możliwych wyrażeń konceptu. Zresztą idea konceptu polega na rozdzieleniu definicji abstrakcyjnego typu od definicji szablonu, którego ten typ może być argumentem. Rozwiazaniem jest napisanie własnego zestawu szablonów, których jedynem zadaniem jest sprawdzanie zgodności przekazanych argumentów szablonu z definiowanym przez ten szablon konceptem. Niestety, można w ten sposób sprawdzać tylko zgodność syntaktyczną.
Idea tworzenia takich szablonów jest prosta (zob. http://www.boost.org/libs/concept_check/concept_check.htm): dla każdego konceptu tworzymy szablon zawierający funkcję constraints(), która zawiera wszystkie możliwe poprawne wyrażenia dla danego konceptu. Np. dla konceptu Comparable możemy zdefiniować:
template<typename T> struct ComparableConcept { void constraints() { require_boolean_expr( a > b); require_boolean_expr( a < b); }; T a,b; };
Szablon require_boolean_expr
template <class TT> void require_boolean_expr(const TT& t) { bool x = t; ignore_unused_variable_warning(x); używa zmiennej x aby kompilator nie generował ostrzeżenia }
sprawdza czy jego argument, a więc wartość zwracana przez operatory, może być konwertowana na bool.
Zwracam uwagę, że nie możemy w kodzie szablonu Comparable użyć defaultowego konstruktora, bo nie jest on wymagany. Dlatego zmienne a i b nie były zdefiniowane wewnątrz funkcji constraints(), tylko jako pola składowe klasy. Ponieważ nie tworzymy żadnej instancji tej klasy, to nie będą wywoływane konstruktory, a więc kompilator nie będzie generował ich kodu.
Teraz potrzebujemy jeszcze sposobu, aby skompilować, ale nie wywołać, funkcję ComparableConcept<T>::constraints(). Możemy tego dokonać pobierając adres funkcji i przypisując go do wskaźnika. Kompilator skompiluje kod funkcji, ale jej nie wykona. Dodatkowo najprawdopodobniej kompilator optymalizujący usunie to przypisanie jako nieużywany kod, ale dopiero po kompilacji (no chyba, że jest bardzo, ale to bardzo inteligentny). Dla wygody opakujemy tę konstrukcję w szablon funkcji:
template <class Concept> inline void function_requires() { void (Concept::*x)() = &Concept::constraints; ignore_unused_variable_warning(x); }
Możemy teraz używać szablonu Comparable w następujący sposób:
main() { function_requires<ComparableConcept<int> >(); function_requires<ComparableConcept<std::complex> >(); błąd }
Bardziej skomplikowane koncepty możemy sprawdzać korzystając z klas sprawdzających dla innych konceptów, np:
template <class Container> struct Mutable_ContainerConcept { typedef typename Container::value_type value_type; typedef typename Container::reference reference; typedef typename Container::iterator iterator; typedef typename Container::pointer pointer;
void constraints() { sprawdzamy czy spełnia wymagania konceptu Container function_requires< ContainerConcept<Container> >(); function_requires< AssignableConcept<value_type> >(); function_requires< InputIteratorConcept<iterator> >();
i = c.begin(); i = c.end(); c.swap(c2); } iterator i; Container c, c2; };
Biblioteka boost, skąd wzięty został ten przykład, posiada implementację szablonów dla każdego konceptu z STL (http://www.boost.org/libs/concept_check/concept_check.htm). Hierachia, którą można tam odczytać, różni się trochę od tej, którą wcześniej zaprezentowałem i która jest opisana w http://www.sgi.com/tech/stl/. Główna różnica to wprowadzenie rozróżnienia pomiędzy kontenerami, które umożliwiaja modyfikację swoich elementów (MutableContainer) i tych, które na to nie pozwalają (Container).
Archeotypy
Klasy sprawdzające koncepty służą do pomocy w implementacji typów będących modelami danego konceptu. Możemy jednak mieć sytuację odwrotną: implementujemy jakiś algorytm ogólny i chcemy się dowiedzieć jaki koncept jest wymagany dla parametrów szablonu? Chcemy wybrać jak najogólniejszy koncept, który jeszcze pozwala na poprawne działanie algorytmu. Pomóc mogą nam w tym archeotypy. Są to klasy, które dokładnie implementują interfejs danego konceptu. Opierając się na http://www.boost.org/libs/concept_check/concept_check.htm, przedstawię teraz implementację archeotypu dla konceptu Comparable.
Koncept Comparable nie wymaga posiadania konstruktora defaultowego, konstruktora kopiujacego oraz operatora przypisania, dlatego w naszym archeotypie zdefiniujemy je jako prywatne:
class comparable_archetype { private: comparable_archetype() {}; comparable_archetype(const comparable_archetype &) {}; comparable_archetype &operator=(const comparable_archetype &) { return *this;}; public: comparable_archetype(dummy_argument) {}; };
Aby móc tworzyć obiekty typu comparable_archetype dodaliśmy niestandardowy konstruktor z argumentem sztucznego typu:
class dummy_argument {};
używanego tylko na tę okazję (jego nazwa powinna być unikatowa).
Operator operator<() nie musi zwracać wartości typu bool, a jedynie wartość typu konwertowalnego na bool, dlatego tworzymy taki typ:
struct boolean_archetype { operator const bool() const {return true;} };
i podajemy go jako typ zwracany przez operatory porównania
boolean_archetype operator<(const comparable_archetype &, const comparable_archetype &){ return boolean_archetype(); }; boolean_archetype operator>(const comparable_archetype &, const comparable_archetype &){ return boolean_archetype(); };
Teraz możemy już przetestować nasz szablon max.
template<typename T> const T &max(const T &a,const T &b) {return (a>b)?a:b;}
main() { comparable_archetype ca(dummy_argument()); max(ca,ca); }
Poprawna kompilacja tego kodu przekonuje nas, że koncept Comparable jest wystarczajacy, przynajmniej syntaktycznie. Proszę zwrócić uwagę, że jeśli użyjemy orginalnego szablonu
template<typename T> T max(T a,T b) {return (a>b)?a:b;}
to kod się nie skompiluje, bo zabraknie konstruktora kopiujacego.
Większość konceptów jest uszczegółowieniem innych konceptów. Implementacja archeotypów w biblitece boost zezwala na takie konstrukcje i gorąco zachęcam do zapoznania się z nią.