Zaawansowane CPP/Wykład 2: Programowanie uogólnione: Różnice pomiędzy wersjami

Z Studia Informatyczne
Przejdź do nawigacjiPrzejdź do wyszukiwania
m Zastępowanie tekstu – „ </math>” na „</math>”
 
(Nie pokazano 95 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 państo już widzą to jest właśnie to do czego
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. Tka
program używajacy szablonów jest od razu programem uogólnionym. Tak
jaki w przypadku tworzenie zwykłych (bez szablonów) programów, trzeba
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ć państwu
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 gra podobną rolę
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 nalężą typu które
rodziny jest określona proceduralnie: do konceptu należą typy, które
spełniają pewne wymagania. Czyli jeśli coś kwacze jak kaczka to to
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 - Standard Template Library) będąca oficjalną częścią
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 nauczanie posługiwania się tą
przykładami z STL-a, ale szczegółowe nauczenie posługiwania się tą
biblioteką '''nie''' jest celem tego wykładu. Powinni jednak państwo
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 wykładzie, oraz wykonywanie podanych ćwiczeń.  
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 <tt>boost</tt> (<tt>$BOOST_HOME/doc/</tt>). Stamtąd
repozytorium bibliotek [http://www.boost.org/ boost]. Stamtąd
tęż będę podawał przykłady i znów gorąco zachęcam państwa do
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, i aby je  podkreślić
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 dana nazwą, nie w momencie
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 ``matki
Zilustrujemy to na przykładzie. W tym celu skorzystamy z "matki
wszystkich przykładów programowania obiektowego'' czyli klasy
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
"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 umiejętność narysowania
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, zakładamy że
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>:
<tt>shape_table</tt>:
Linia 88: Linia 88:
  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>
<font color=red>{mod02/code/shape.html}{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 tablice <tt>shape_table</tt> i jak
jest przechowywany w danym elemencie tablicy <tt>shape_table</tt> i jak
jest zaimplementowana funkcja <tt>draw</tt>. Istotne jest że każdy obiekt
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
<tt>draw</tt>.  Innymi słowy programista korzysta tu tylko ze znajomości i
dostępności interfejsu obiektów typy kształt, a resztę wykonuje
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ę
Linia 111: Linia 110:
   virtual void draw() = 0;
   virtual void draw() = 0;
   virtual ~Shape() {};
   virtual ~Shape() {};
  };
};


<font color=red>{mod02/code/shape.html}{kod źródłowy}</font>
([[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 <tt>Shape</tt> jest klasą abstrakcyjną ponieważ zawiera
kształty. Klasa <tt>Shape</tt> jest klasą abstrakcyjną, ponieważ zawiera
nie zaimplementowaną wirtualną  czystą fukcję <tt>void draw()</tt>.
niezaimplementowaną wirtualną  czystą fukcję <tt>void draw()</tt>.
Kod definiujący konkretne klasy kształtów może wygladać następująco:
Kod definiujący konkretne klasy kształtów może wyglądać następująco:


  class Rectangle: public Shape {
  class Rectangle: public Shape {
Linia 135: Linia 134:
  };
  };


<font color=red>{mod02/code/shape.html}{kod źródłowy}</font>
([[media:Rectangle.h | Źródło rectangle.h]])


i
i
Linia 150: Linia 149:
  };
  };


<font color=red>{mod02/code/shape.html}{kod źródłowy}</font>
([[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:
Linia 159: Linia 158:
  }  
  }  


<font color=red>{mod02/code/shape.html}{kod źródłowy}</font>
([[media:Draw.cpp | Źródło draw.cpp]])


Funkcja \cd{draw_shapes} wykorzystuje zachowanie polimorficzne: to
Funkcja <tt>draw_shapes</tt> wykorzystuje zachowanie polimorficzne: to
która funkcja \cd{draw} zostanie wywołana zależy od tego jaki
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
Linia 174: Linia 173:
   draw_shapes(list,4);
   draw_shapes(list,4);
  }
  }
 
<font color=red>kod źródłowy</font>
<font color=red>{mod02/code/shape.html}{kod źródłowy}</font>


W ten sposób zaimplementowaliśmy podstawowy paradygmat programowania
W ten sposób zaimplementowaliśmy podstawowy paradygmat programowania
Linia 185: Linia 183:
==Polimorfizm statyczny==
==Polimorfizm statyczny==


Patrzac na kod funkcji <tt>draw_shapes</tt> możemy zauważyć że korzysta on
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
<tt>draw()</tt>. To sygnatura, czyli typ parametru wywołania tej funkcji
określa że musi to być wskaźnik na typ <tt>Shape</tt>.  Z poprzedniego
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:


Linia 197: Linia 195:
  }  
  }  


<font color=red>{mod02/code/shape_template.html}{kod źródłowy}</font>
([[media:Draw_template.h | Źródło draw_template.h]])


taką funkcję możemy wywołać dla dowolnej tablicy, byle tylko
Taką funkcję możemy wywołać dla dowolnej tablicy, byle tylko
przechowywany typ posiadał metodę <tt>draw</tt>. Mogą to być obiekty typów
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>
<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:


  class Drawable {
  class Drawable {
Linia 216: Linia 214:
  }
  }


<font color=red>{mod02/code/shape_template.html}{kod źródłowy}</font>
<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
Linia 223: Linia 221:
podstawie typu tablicy jaką funkcję <tt>draw</tt> wywołać. Oczywiście w
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 na poprzednim wykładzie.
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ę zbrane głowne właściwości każdego podejścia.
podaję zebrane głowne właściwości każdego podejścia.
 
1. Dziedzieczenie i funkcje wirtualne
 
&nbsp;&nbsp;&nbsp;&nbsp;(a) Umożliwia pracę ze zbiorami niejednorodnych obiektów i korzysta
z polimorfizmu dynamicznego.
 
&nbsp;&nbsp;&nbsp;&nbsp;(b) Wymaga wspólnej hierarchi dziedziczenia.
 
&nbsp;&nbsp;&nbsp;&nbsp;(c) Wymusza korzystanie ze wskaźników lub referencji i funkcji
wirtualnych.
 
&nbsp;&nbsp;&nbsp;&nbsp;(d) Zazwyczaj generuje mniejszy kod.
 
2. Szablony
 
&nbsp;&nbsp;&nbsp;&nbsp;(a) Implementuje polimorfizm statyczny.


&nbsp;&nbsp;&nbsp;&nbsp;(b) Bezpiecznie obsługuje jednorodne zbiory obiektów
# Dziedzieczenie i funkcje wirtualne
 
## umożliwia pracę ze zbiorami niejednorodnych obiektów i korzysta z polimorfizmu dynamicznego
&nbsp;&nbsp;&nbsp;&nbsp;(c) Nie trzeba korzystać ze wskaźników i referencji ani funkcji wirtualnych.
## wymaga wspólnej hierarchii dziedziczenia
 
## wymusza korzystanie ze wskaźników lub referencji i funkcji wirtualnych
&nbsp;&nbsp;&nbsp;&nbsp;(d) Nie musimy korzystać ze wspólnej hierarchii dziedziczenia.
## 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==
==Koncepty==


Przyjrzyjmy się jeszcze raz deklaracji funkcji <tt>draw_shapes</tt> i
Przyjrzyjmy się jeszcze raz deklaracji funkcji <tt>draw_shapes</tt> i
<tt>draw_template</tt>: Kiedy programista widzi deklarację
<tt>draw_template</tt>. Kiedy programista widzi deklarację:
   
   
  void draw_shapes(Shape *table[])
  void draw_shapes(Shape *table[])


wie że interfejs wykorzystywany przez funkcję <tt>draw</tt> jest
wie, że interfejs wykorzystywany przez funkcję <tt>draw</tt> jest
zdefiniowany przez klasę <tt>Shape</tt>. Aby go poznać musi przeczytać kod
zdefiniowany przez klasę <tt>Shape</tt>. Aby go poznać musi przeczytać kod
i dokumentację tej klasy.  
i dokumentację tej klasy.  
Natomiast kiedy programista widzi deklarację  
Natomiast kiedy programista widzi deklarację:


  template<typename T> void draw_template(T table[],size_t n);
  template<typename T> void draw_template(T table[],size_t n);


to musi przesledzić kod funkcji <tt>draw_templates</tt> aby poznać
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
ograniczenia nałożone na argument szablonu <tt>T</tt>. W tym przypadku nie
jest to trudne, ale ogólnie może to być zadanie nietrywialne.
jest to trudne, ale ogólnie może to być nietrywialne zadanie.


Zamiast jednak definiować ograniczenia i warunki dla każdego szablonu
Zamiast jednak definiować ograniczenia i warunki dla każdego szablonu
Linia 280: 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 hierarachi
Koncepty mogą tworzyć hierachie analogiczne do hierarachii
dziedziecznia. Mówimy że koncept <tt>A</tt> jest bardziej wyspecjalizowany
dziedziecznia. Mówimy, że koncept <tt>A</tt> jest bardziej wyspecjalizowany
niż <tt>B</tt> (<tt>A</tt> is-refinement-of <tt>B</tt>) jeśli zestaw ograniczeń
niż <tt>B</tt> (<tt>A</tt> is-refinement-of <tt>B</tt>), jeśli zestaw ograniczeń
konceptu <tt>B</tt> zawiera się w zestwie ograniczeń konceptu <tt>A</tt>.
konceptu <tt>B</tt> zawiera się w zestwie ograniczeń konceptu <tt>A</tt>.
Będę też używał określenia <tt>A</tt> jest "uszczegółowieniem" <tt>B</tt>.   
Będę też używał określenia <tt>A</tt> jest "uszczegółowieniem" <tt>B</tt>.   


Pojęcie konceptu gra więc przy programowaniu za pomocą szablonów
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
"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:<br><br>
mogą zawierać między  innymi:
 
1. Prawidłowe wyrażenia. Zestaw wyrażeń języka C++ które muszą się
poprawnie kompilować<br><br>
 
2. Typy stowarzyszone. Ewentualne dodatkowe typy występujące w
prawidłowych wyrażeniach.<br><br>
 
3. 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.<br><br>


4. Złożoność algorytmów. Gwarancje co do czasu i  
# Prawidłowe wyrażenia. Zestaw wyrażeń języka C++, które muszą się poprawnie kompilować.
innych zasobów potrzebnych do wykonania danego  
# Typy stowarzyszone. Ewentualne dodatkowe typy występujące w prawidłowych wyrażeniach.
wyrażenia.<br><br>
# 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
Programowanie uogólnione polega więc na wyszukiwaniu konceptów na
tylke ogólnych aby pasowały do dużej liczby typów i na tyle
tyle ogólnych, aby pasowały do dużej liczby typów i na tyle
szczegółowych aby zezwalały na wydajną implementację.  
szczegółowych, aby zezwalały na wydajną implementację.  


===Definiowanie konceptów===
===Definiowanie konceptów===
Linia 323: Linia 301:
  template<typename T> max(T a,T b) {return (a>b)?a:b;}
  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 <tt>T</tt> aby
Zacznijmy od gramatyki.  Jakie warunki musi spełniać typ <tt>T</tt>, aby
podstawienie go jako argument szablonu <tt>max</tt> dawało poprawne
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 <tt>bool operator>(...)</tt>. Specjalnie
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 <tt>operator>(...)</tt> może być
jak parametry są przekazywane, co więcej <tt>operator>(...)</tt> może być
zadefiniowany jako składowa klasy i posiadać tylko jeden jawny argument.  
zdefiniowany jako składowa klasy i posiadać tylko jeden jawny argument.  
Ważne jest to że jeśli <tt>x</tt> i <tt>y</tt> są obiektami typu <tt>T</tt>
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:


Linia 340: Linia 318:
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 <tt>T</tt> musi posiadać konstruktor kopiujący. Oznacza to że  
wartość, to typ <tt>T</tt> musi posiadać konstruktor kopiujący. Oznacza to, że  
jeśli <tt>x</tt> i <tt>y</tt> są obiektami typu <tt>T</tt> to wyrażenia:
jeśli <tt>x</tt> i <tt>y</tt> są obiektami typu <tt>T</tt> to wyrażenia:


Linia 351: Linia 329:
są poprawne.
są poprawne.


{{przyklad|2.1||}}
{{kotwica|przyklad.2.1|}}{{przyklad|2.1||}}


Spełnienie obudwu tych warunków zapewni nam poprawność gramatyczną
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 poprawnośćią semantyczna? Mogłoby sie wydawać że jest bez znaczenia  
A co z poprawnością semantyczną? Mogłoby sie wydawać, że jest bez znaczenia  
jak zdefiniujemy <tt>operator>(...)</tt>.  
jak zdefiniujemy <tt>operator>(...)</tt>.  
Koncept  typu <tt>T</tt> jest jednak częścią kontraktu dla
Koncept  typu <tt>T</tt> jest jednak częścią kontraktu dla
funkcji <tt>max</tt>. Kontraktu zawieranego pomiędzy twórcą tego wielce
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 a typach zgodnych z
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
Linia 369: Linia 347:
większy od wyniku, czyli wyrażenie
większy od wyniku, czyli wyrażenie


{{kotwica|prz.2.1|}}
<center><math>
<center><math>
!( a> max(a,b) ) \wedge !(b> max(a,b))
!( 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ę
<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 równośći:
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 <font color=red>{##eq:max-post|Uzupelnic eq:max-post|}</font> nie może być prawdziwe dla żadnej
to wyrażenie [[#prz.2.1|2.1]] nie może być prawdziwe dla żadnej
wartości <tt>a</tt> i <tt>b</tt>. Aby funkcja \cd{max} mogła spełnić swój
wartości <tt>a</tt> i <tt>b</tt>. Aby funkcja <tt>max</tt> mogła spełnić swój
warunek końcowy musimy narzycić pewne ograniczenia semantyczne na
warunek końcowy musimy narzucić pewne ograniczenia semantyczne na
<tt>operator>()</tt>. Te warunki to żądanie aby relacja większości
<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>&nbsp;&nbsp;i&nbsp;&nbsp;<math>(x>y) \wedge (y>z) => (x>z) </math></center>
<center><math>(x>x) = false</math>&nbsp;&nbsp;i&nbsp;&nbsp;<math>(x>y) \wedge (y>z) => (x>z)</math></center>


To rozumowanie można by ciagnąć dalej i zauważyć że nawet z tym
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>.
<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 <font color=red>{##ex:copy-constr|Uzupelnic ex:copy-constr|}</font> powoduje powstanie kopii obiektu <tt>x</tt>
operacji [[#prz.2.1|2.1]] powoduje powstanie kopii obiektu <tt>x</tt>
(cokolwiekby to nie znaczyło).
(cokolwiek by to nie znaczyło).


===Comparable i Assignable===
===Comparable i Assignable===
 
Reasumując, dostajemy zbiór warunków, które musi
Zbierając to wszystko do kupy, dostajemy zbiór warunków który musi
spełniać typ <tt>T</tt>, aby móc go podstawić do szablonu funkcji <tt>max</tt>.
spełniać typ <tt>T</tt> aby móc go podstawić do szablonu funkcji <tt>max</tt>.
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
<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 posiadanie konstruktora kopiujacego
zrezygnować z konieczności posiadania konstruktora kopiujacego,
zmieniając deklarację <tt>max</tt> na:
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
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 ``kopiowalne''.  Dalej możemy
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 operator < poprzez:
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;};
Linia 428: Linia 406:


Tak więc dochodzimy do dwu konceptów: <tt>Comparable</tt> reprezentującego
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 <tt><</tt> i
typy, których obiekty można porównywać za pomocą operatorów <tt><</tt> i
<tt>></tt> <font color=red>(zob. rys.)</font>, oraz <tt>Assignable</tt> reprezentujacego typy których
<tt>></tt> oraz <tt>Assignable</tt> reprezentujacego typy, których
obiekty możemy kopiować i przypisywać do siebie <font color=red>(zob. rys.)</font>.  Taką
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
<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 włączyny do
zależną od rozpatrywanego problemu. O tym czy dwa pojęcia włączymy do
jednego konceptu czy nie decyduje np. odpwiedź na pytanie czy
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 442: 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 przedtawione są na stronach firmy SGI <font color=red>(<tt>SGI_STL_HOME</tt>)</font> dokąd państwa odsyłam.
koncepty STL przedstawione są na stronach firmy [http://www.sgi.com/tech/stl/ SGI] dokąd Państwa odsyłam.


==STL==
==STL==
Linia 451: 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 bibliotekę można patrzeć więc dwojako: jako rozszerzenie języka
Na bibliotekę można patrzeć więc dwojako: jako rozszerzenie języka
C++ dadatkowe funkcje, lub jako na zbiór konceptów stanowiących
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ć państwu prace programistyczne.
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 466: 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ą"
<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:
Linia 473: Linia 451:
  v[8]<nowiki>=</nowiki>1;
  v[8]<nowiki>=</nowiki>1;


a listy przeglądamy po kolei przesuwając się o jeden element wprzód czy w tył
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 !!! -->//
  <i>Uwaga! To nie jest kod STL-owy !!!</i>
  lista<int> l;
  lista<int> l;
  l.reset(); //<-- ustwia element bieżacy na początek listy -->//
  l.reset(); <i>ustawia element bieżacy na początek listy</i>
  for(int i<nowiki>=</nowiki>0;i<8;i++)
  for(int i<nowiki>=</nowiki>0;i<8;i++)
       l.next(); //<-- przesuwa element bieżący o jeden element do przodu -->//<br>
       l.next(); <i>przesuwa element bieżący o jeden element do przodu</i><br>
  l.current()<nowiki>=</nowiki>1; /* zwraca referencję do elementu bieżącego
  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 497: Linia 475:


  std::list<int> l;
  std::list<int> l;
  //<-- tu jakoś inicjalizujemy liste -->//
  <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++) {
       //<-- każdy kontener definiuje typ stowarzyszony nazwany <tt>iterator</tt> -->//
       <i>każdy kontener definiuje typ stowarzyszony nazwany <tt>iterator</tt></i>
   cout<<*it<<endl;
   cout<<*it<<endl;
       //<-- korzystamy z iteratorów jak ze zwykłych wskaźników -->//
       <i>korzystamy z iteratorów jak ze zwykłych wskaźników</i>
   }
   }
  }
  }
Linia 515: Linia 493:
  }
  }


[http://osilek.mimuw.edu.pl/images/9/98/Accumulate.cpp Źródło: Accumulate.cpp}]
([[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 wectora zezwala na konstrukcje
a wektorem. Dlatego np. iterator wektora zezwala na konstrukcje
<tt>it[i]</tt> a iterator listy już nie. Oznacza to że algorytm który
<tt>it[i]</tt>, a iterator listy już nie. Oznacza to, że algorytm, który
działa dla iteratorów wektora (np. <tt>sort</tt>) nie musi działać dla
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
<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
<tt>std::list<T>::iterator</tt>. Zobaczymy to w następnej części tego
wykładu.
wykładu.
Linia 531: 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:


1. 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>]
* <font color=red>{<tt>SGI_STL_HOME/Vector.html</tt></tt>}{<tt>vector</tt>}</font>
## [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:
* <font color=red>{<tt>SGI_STL_HOME/Deque.html</tt>}{<tt>deque</tt>}</font>
## [http://www.sgi.com/tech/stl/set.html <tt>set</tt>]
 
## [http://www.sgi.com/tech/stl/Map.html <tt>map</tt>]
* <font color=red>{<tt>SGI_STL_HOME/List.html</tt>}{<tt>list</tt>}</font>
## [http://www.sgi.com/tech/stl/multiset.html <tt>multiset</tt>]
 
## [http://www.sgi.com/tech/stl/Multimap.html <tt>multimap</tt>]
2. Kontenery asocjacyjne czyli pojemniki w których klient nie ma
kontroli nad kolejnością elementów, są to:
 
* <font color=red>{<tt>SGI_STL_HOME/set.html</tt>}{<tt>set</tt>}</font>
 
* <font color=red>{<tt>SGI_STL_HOME/map.html</tt>}{<tt>map</tt>}</font>
 
* <font color=red>{<tt>SGI_STL_HOME/multiset.html</tt>}{<tt>multiset</tt>}</font>
 
* <font color=red>{<tt>SGI_STL_HOME/multimap.html</tt>}{<tt>multimap</tt>}</font>


Ponadto różni dostawcy oferują dodatkowe pojemniki. Na uwagę zasługuje
Ponadto różni dostawcy oferują dodatkowe pojemniki. Na uwagę zasługuje
Linia 555: Linia 523:
miedy innymi wchodzi w skład pakietu g++ i dostarcza dodatkowo takich
miedy innymi wchodzi w skład pakietu g++ i dostarcza dodatkowo takich
kontenerów jak: lista jednokierunkowa <tt>slist</tt> oraz tablice
kontenerów jak: lista jednokierunkowa <tt>slist</tt> oraz tablice
haszujące <tt>hash_set</tt> czy <tt>hash_map</tt> <ref name="drugi">SGI</ref> . Hierachie
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 <font color=red>{rysunek~[##fig:sequence|Uzupelnic fig:sequence|]}</font>, a kontenerów asocjacyjnych <font color=red>{rysunek~[##fig:associate|Uzupelnic fig:associate|]}</font>.
konceptów kontenerów typu sekwencji przedstawia [[#rys.2.1|rysunek 2.1]], a kontenerów asocjacyjnych [[#rys.2.2|rysunek 2.2]].


<font color=red>[p][height=\textwidth,angle=90]{mod02/graphics/sequence.eps}<br>
{{kotwica|rys.2.1|}}<center>
{Hierarchia konceptów dla pojemników typu sekwencyjnego}</font>
[[File:cpp-2-sequence.svg|600x400px|thumb|center|Rysunek 2.1. Hierarchia konceptów dla pojemników typu sekwencyjnego.]]
</center>


<font color=red>[p][height=\textwidth,angle=90]{mod02/graphics/associate.eps}<br>
{{kotwica|rys.2.2|}}<center>
{Hierarchia konceptów dla pojemników typu asocjacyjnego}</font>
[[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ólowe opisy
Nie będę tu omawiał tych wszystkich konceptów. Ich szczegółowe opisy
znajdują się na stronie <font color=red>({<tt>SGI_STL_HOME</tt>})</font>. Prowadzą do niej linki z
znajdują się na stronie [http://www.sgi.com/tech/stl/ http://www.sgi.com/tech/stl/]. Tutaj chciałbym tylko dodać parę luźnych
rysunków oraz z tej strony. Tutaj chciałbym tylko dodać parę luźnych
komentarzy.
komentarzy.


Po pierwsze rodzi się pytanie czy taka skomplikowana taxonomia jest
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. Rzeczywiśc do posługiwania się biblioteką
więcej niż typów kontenerów. Rzeczywiście, do posługiwania się biblioteką
w zasadzie wystarczy zaznajomić się z opisami kontenerów i hierachią
w zasadzie wystarczy zaznajomić się z opisami kontenerów i hierarchią
iteratorów <font color=red>{rysunek&nbsp;[##fig:iteratory|Uzupelnic fig:iteratory|]}</font>.  Podane klasyfikacje przydają się dopiero kiedy dodajemy własne elementy do biblioteki.
iteratorów (zob. [[#rys.2.3|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
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 komponetami biblioteki, czy kodem innych developerów.
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:


Linia 591: Linia 560:
  int i;<br>
  int i;<br>
  std::vector<int> v(1);
  std::vector<int> v(1);
  std::vector<int> buff(100); //<-- staramy się zająć pamięć za v -->//<br>
  std::vector<int> buff(100); <i>staramy się zająć pamięć za v</i><br>
  v[0]<nowiki>=</nowiki>0;
  v[0]<nowiki>=</nowiki>0;
  it<nowiki>=</nowiki>v.begin();
  it<nowiki>=</nowiki>v.begin();
  i<nowiki>=</nowiki>(*it); //<-- OK, przypisuje i<nowiki>=</nowiki>0 -->//
  i<nowiki>=</nowiki>(*it); <i>OK, przypisuje i<nowiki>=</nowiki>0</i>
  for(int i<nowiki>=</nowiki>0;i<10;++i)
  for(int i<nowiki>=</nowiki>0;i<10;++i)
   v.push_back(i);   
   v.push_back(i);   
     //<-- ponieważ przekraczamy koniec wektora, kontener zaalokuje dodatkową pamięc. Może
     <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  
     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 -->//<br>
     pamięci. To spowoduje, że wskaźnik it przestanie pokazywać na początek wektora v</i><br>
  std::cerr<<(*it)<<std::endl ;                  //<-- niezdefiniowane -->//<br>
  std::cerr<<(*it)<<std::endl ;                  <i>niezdefiniowane</i><br>
  std::cerr<<"iterator nieprawidlowy"<<std::endl;  
  std::cerr<<"iterator nieprawidlowy"<<std::endl;  
  for(;it !<nowiki>=</nowiki> v.end(); ++it)  //<-- potencjalnie nieskończona pętla -->//  
  for(;it !<nowiki>=</nowiki> v.end(); ++it)  <i>potencjalnie nieskończona pętla</i>   
   std::cerr<<*it<<std::endl;
   std::cerr<<*it<<std::endl;
  ;<br>   
  ;<br>   
Linia 610: Linia 579:
  ;   
  ;   


[http://osilek.mimuw.edu.pl/images/f/f7/Invalid.cpp Źródło: Invalid.cpp]
([[media:Invalid.cpp | Źródło: invalid.cpp]])


Bardzo państwa na ten problem uczulam. Efekt działania powyższego kodu
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); //<-- staramy się zająć pamięć za v -->//
  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 <ref name="pierwszy">Wbrew pozorom jest to najgorsza
program zadziała poprawnie (wbrew pozorom jest to najgorsza
możliwa sytuacja!</ref> .
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 operacja indeksowania
będzie jednak czas ich wykonywania. I tak rząd O(1) jest gwarantowany w operacji indeksowania
wektora jest gwarantowana być rzędu O(1). Natomiast operacja dodania
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 posiadaja operacji indeksowania.  
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 636: 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 <font color=red>{rysunek&nbsp;[##fig:iteratory|Uzupelnic fig:iteratory|]}</font>. Zaznaczono na nim również które  
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.


<font color=red>[p][height<nowiki>=</nowiki>,angle<nowiki>=</nowiki>90]{mod02/graphics/iteratory.eps}<br>
{{kotwica|rys.2.3|}}<center>
{Hierarchia konceptów dla iteratorów}</font>
[[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 typy,
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 650: 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 [<tt>it1</tt>,<tt>it2</tt>).   
końcem zakresu.  Zakres oznaczamy poprzez [<tt>it1</tt>,<tt>it2</tt>) (zob. [[#rys.2.4|rysunek 2.4]]).   


<font color=red>{mod02/graphics/zakres.eps}</font>
{{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.


double x[10];
<Source>
double *end<nowiki>=</nowiki>&x[10];
double x[10];
//<-- zwykłe wskażniki mogą być użyte jako iteratory -->//
double *end=&x[10];
std::cout<<accumulate(x,end,0)<<endl; //<-- suma elementów tablicy -->//
//zwykłe wskażniki mogą być użyte jako iteratory
std::cout<<accumulate(x,end,0)<<endl; <i>suma elementów tablicy</i>
</Source>


Każdy kontener posiada motody <tt>begin()</tt> i <tt>end()</tt> zwracające
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
wyglada więc następująco:
wygląda więc następująco:


typedef vector<int>::iterator iterator;
<Source>
vector<it> v(100);
typedef vector<int>::iterator iterator;
for(iterator it<nowiki>=</nowiki>v.begin();it!<nowiki>=</nowiki>v.end();++it) {
vector<it> v(100);
for(iterator it=v.begin();it!=v.end();++it) {
  ...
  ...
}
}
</Source>


[http://osilek.mimuw.edu.pl/images/9/98/Accumulate.cpp Źródło: Accumulate.cpp]
([[media:Accumulate.cpp | Źródło: accumulate.cpp]])


Proszę zwrócić uwagę na wykorzystanie operator <tt><nowiki>=</nowiki></tt> do sprawdzenia
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 <tt>operator<()</tt>. Reszta jest tylko
porównywane za pomocą operatora <tt>operator<()</tt>. Reszta jest tylko
Linia 690: 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 algorytmu ogólne nie mogą
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
<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 700: 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ś na do czego można zastosować składnię
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 <tt>operator()(...)</tt> .
oraz obiekty klas, w których zdefiniowano <tt>operator()(...)</tt> .


Funktory w STL są podzielone ze względu na liczę argumentów wywołania.
Funktory w STL są podzielone ze względu na liczbę argumentów wywołania.
<tt>Generator</tt> nie przyjmują żadnego argumentu, <tt>UnaryFunction</tt>
<tt>Generator</tt> nie przyjmuje żadnego argumentu, <tt>UnaryFunction</tt>
posiada jeden argument a <tt>BinaryFunction</tt> dwa argumenty wywołania.
posiada jeden argument, a <tt>BinaryFunction</tt> - dwa argumenty wywołania.
Ważną podklasą są funkcje zwracające wartość typu <tt>bool</tt> nazywane
Ważną podklasą są funkcje zwracające wartość typu <tt>bool</tt>, nazywane
predykatami. Rozróżniamy więc  <tt>UnaryPredicate</tt> i
predykatami. Rozróżniamy więc  <tt>UnaryPredicate</tt> i
<tt>BinaryPredicate</tt>.
<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:


Linia 725: Linia 701:
  };
  };


[http://osilek.mimuw.edu.pl/images/0/0a/Bind.cpp Źródło: Bind.cpp]
([[media:Bind.cpp | Źródło: bind.cpp]])


Za pomocą obiektu klasy {SequenceGen} możemy wypełnić wektor
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:


Linia 734: Linia 710:
  generate_n(v.begin(),n,SequenceGen<int>(1,2));
  generate_n(v.begin(),n,SequenceGen<int>(1,2));


[http://osilek.mimuw.edu.pl/images/0/0a/Bind.cpp Źródło: Bind.cpp]
([[media:Bind.cpp | Źródło: bind.cpp]])


Standardowy algorytm
Standardowy algorytm
Linia 745: Linia 721:
wyników wywołania funktora <tt>gen</tt>.  Powyższy kod ilustruje typowy
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 757: Linia 733:


Który przeszukuje zakres <tt>[first,last)</tt> do napotkania pierwszego
Który przeszukuje zakres <tt>[first,last)</tt> do napotkania pierwszego
elementu dla którego predykat <tt>pred</tt> jest prawdziwy i zwraca
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
<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 adapteru funkcji
czterech. Zamiast go implementować skorzystamy z adaptera funkcji
<tt>bind2nd</tt>.  Ten obiekt przyjmuje funktor dwuargumentowy
<tt>bind2nd</tt>.  Ta funkcja przyjmuje funktor dwuargumentowy
(<tt>AdaptableBinaryFunction</tt>) <tt>F(T,U)</tt> i jakąś wartość <tt>x</tt> typu
(<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
<tt>U</tt> i zwraca funktor jednoparametrowy <tt>F(T,x)</tt>. Korzystając z
Linia 772: Linia 748:
   cout<<*it<<endl;
   cout<<*it<<endl;
  else
  else
   cout<<"nie znaleziono zadanego elementy";
   cout<<"nie znaleziono zadanego elementu";
  }
  }


[http://osilek.mimuw.edu.pl/images/0/0a/Bind.cpp Źródło: Bind.cpp]
([[media:Bind.cpp | Źródło: bind.cpp]])


STL wprowadza więc do C++ elementy programowanie funkcyjnego.
STL wprowadza więc do C++ elementy programowania funkcyjnego.


==Debugowanie==
==Debugowanie==
Linia 784: 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 mechanismu pozwalającego na
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 <font color=red>(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
{<tt>BOOST_LIBS/concept_check/concept_check.htm</tt>})</font>: dla każdego
konceptu tworzymy szablon zawierający funkcję <tt>constraints()</tt>, która
konceptu tworzymy szablon zawierający funkcję <tt>constraints()</tt> która
zawiera wszystkie możliwe poprawne wyrażenia dla danego konceptu. Np.
zawiera wszystkie możliwe poprawne wyrażenia dla danego konceptu. Np.
dla konceptu <tt>Comparable</tt> możemy zdefiniować:
dla konceptu <tt>Comparable</tt> możemy zdefiniować:
Linia 817: Linia 792:
  };
  };


[http://osilek.mimuw.edu.pl/images/f/fc/Concept_check.cpp Żródło: Concept_check.cpp]
([[media:Concept_check.cpp | Źródło: concept_check.cpp]])


szablon \cd{require_boolean_expr}
Szablon <tt>require_boolean_expr</tt>


  template <class TT>
  template <class TT>
Linia 825: Linia 800:
     bool x = t;
     bool x = t;
     ignore_unused_variable_warning(x);
     ignore_unused_variable_warning(x);
     //<-- używa zmiennej {\tt x} aby kompilator nie generował ostrzeżenia -->//
     <i>używa zmiennej <tt>x</tt> aby kompilator nie generował ostrzeżenia</i>
  }
  }


[http://osilek.mimuw.edu.pl/images/f/fc/Concept_check.cpp Żródło: Concept_check.cpp]
([[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 <tt>bool</tt>.
może być konwertowana na <tt>bool</tt>.


Zwracam uwagę że nie możemy w kodzie szablony <tt>Comparable</tt> użyć
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>a</tt> i <tt>b</tt> nie były zdefiniowane wewnątrz funkcji
<tt>constraints()</tt> tylko jako pola składowe klasy. Ponieważ nie
<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 wykonać
Teraz potrzebujemy jeszcze sposobu, aby skompilować, ale nie wywołać,
funkcję <tt>ComparableConcept<T>::constraints()</tt>. Możemy
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 nie używany kod, ale dopiero po kompilacji (no chyba że jest
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
konstrukcję w szablon funkcji:
konstrukcję w szablon funkcji:


Linia 855: Linia 830:
  }
  }


[http://osilek.mimuw.edu.pl/images/f/fc/Concept_check.cpp Żródło: Concept_check.cpp]
([[media:Concept_check.cpp | Źródło: concept_check.cpp]])


Możemy teraz używać szablony <tt>Comparable</tt> w następujacy sposób:
Możemy teraz używać szablonu <tt>Comparable</tt> w następujący sposób:


  main() {
  main() {
   function_requires<ComparableConcept<int> >();
   function_requires<ComparableConcept<int> >();
   function_requires<ComparableConcept<std::complex> >(); //<-- bład -->//
   function_requires<ComparableConcept<std::complex> >(); <i>błąd</i>
  }
  }


[http://osilek.mimuw.edu.pl/images/f/fc/Concept_check.cpp Żródło: Concept_check.cpp]
([[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:


  template <class Container>
  template <class Container>
Linia 877: Linia 852:
   typedef typename Container::pointer pointer;<br>
   typedef typename Container::pointer pointer;<br>
   void constraints() {
   void constraints() {
     //<-- sprawdzamy czy spełnia wymagania konceptu <tt>Container</tt> -->//
     <i>sprawdzamy czy spełnia wymagania konceptu <tt>Container</tt></i>
     function_requires< ContainerConcept<Container> >();
     function_requires< ContainerConcept<Container> >();
     function_requires< AssignableConcept<value_type> >();
     function_requires< AssignableConcept<value_type> >();
Linia 889: Linia 864:
  };
  };


Biblioteka <tt>boost</tt> skąd wzięty został ten przykłąd posiada
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 <font color=red>{(<tt>BOOST_LIBS/concept_check/concept_check.htm</tt>})</font>. Hierachia którą
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 <font color=red>{<tt>SGI_STL_HOME</tt>}</font>. Główna
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 (<tt>MutableContainer</tt>) i tych
umożliwiaja modyfikację swoich elementów (<tt>MutableContainer</tt>) i tych,
które na to nie pozwalają (<tt>Container</tt>).
które na to nie pozwalają (<tt>Container</tt>).


Linia 904: 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. Opierając się na
dokładnie implementują interfejs danego konceptu. Opierając się na
<font color=red>{<tt>BOOST_LIBS/concept_check/concept_check.htm</tt>}</font> przedstawię teraz
[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 <tt>Comparable</tt>. Koncept
implementację archeotypu dla konceptu <tt>Comparable</tt>.
<tt>Comparable</tt> nie wymaga posiadana konstruktora defaultowego,
 
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:


Linia 923: Linia 900:
  };
  };


[http://osilek.mimuw.edu.pl/images/e/ed/Archeotype.cpp Źródło: Archeotype.cpp]
([[media:Archeotype.cpp | Źródło: archeotype.cpp]])


Aby móc tworzyć obiekty typu <tt>comparable_archetype</tt> dodaliśmy
Aby móc tworzyć obiekty typu <tt>comparable_archetype</tt> dodaliśmy
Linia 930: Linia 907:
  class dummy_argument {};
  class dummy_argument {};


używanego tylko na okazję (jego nazwa powinna być unikatowa).
używanego tylko na okazję (jego nazwa powinna być unikatowa).


Operator <tt>operator<()</tt> nie musi zwracać wartości typu <tt>bool</tt> a
Operator <tt>operator<()</tt> nie musi zwracać wartości typu <tt>bool</tt>, a
jedynie wartość typu konwertowalnegona <tt>bool</tt>, dlatego tworzymy taki typ:
jedynie wartość typu konwertowalnego na <tt>bool</tt>, dlatego tworzymy taki typ:


  struct boolean_archetype  {
  struct boolean_archetype  {
Linia 950: Linia 927:
  };
  };


[http://osilek.mimuw.edu.pl/images/e/ed/Archeotype.cpp Źródło: Archeotype.cpp]
([[media:Archeotype.cpp | Źródło: archeotype.cpp]])


Teraz możemy już przetestować nasz szablon <tt>max</tt>.
Teraz możemy już przetestować nasz szablon <tt>max</tt>.
Linia 961: Linia 938:
  }
  }


[http://osilek.mimuw.edu.pl/images/e/ed/Archeotype.cpp Źródło: Archeotype.cpp]
([[media:Archeotype.cpp | Źródło: archeotype.cpp]])


Poprawna kompilacja tego kodu przekonuje nas że koncept
Poprawna kompilacja tego kodu przekonuje nas, że koncept
<tt>Comparable</tt> jest wystarczajacy, przynajmniej syntaktycznie. Proszę
<tt>Comparable</tt> jest wystarczajacy, przynajmniej syntaktycznie. Proszę
zwrócić uwagę że jeśli użyjemy orginalnego szablonu
zwrócić uwagę, że jeśli użyjemy orginalnego szablonu


  template<typename T> T max(T a,T b) {return (a>b)?a:b;}
  template<typename T> T max(T a,T b) {return (a>b)?a:b;}


[http://osilek.mimuw.edu.pl/images/e/ed/Archeotype.cpp Źródło: Archeotype.cpp]
([[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 <tt>boost</tt> <font color=red>{(<tt>BOOST_LIBS/concept_check/concept_check.htm</tt>)}</font> zezwala na takie
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 nia.
konstrukcje i gorąco zachęcam do zapoznania się z nią.
 
==Podsumowanie==
 
==Przypisy==
<references/>

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() {};
};

( Źródło shape.h)

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;};
};

( Źródło rectangle.h)

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;}; };

( Źródło circle.h)

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();
} 

( Źródło draw.cpp)

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();
} 

( Źródło draw_template.h)

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.

  1. Dziedzieczenie i funkcje wirtualne
    1. umożliwia pracę ze zbiorami niejednorodnych obiektów i korzysta z polimorfizmu dynamicznego
    2. wymaga wspólnej hierarchii dziedziczenia
    3. wymusza korzystanie ze wskaźników lub referencji i funkcji wirtualnych
    4. zazwyczaj generuje mniejszy kod.
  2. Szablony
    1. implementuje polimorfizm statyczny
    2. bezpiecznie obsługuje jednorodne zbiory obiektów
    3. nie trzeba korzystać ze wskaźników i referencji ani funkcji wirtualnych
    4. 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:

  1. Prawidłowe wyrażenia. Zestaw wyrażeń języka C++, które muszą się poprawnie kompilować.
  2. Typy stowarzyszone. Ewentualne dodatkowe typy występujące w prawidłowych wyrażeniach.
  3. 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.
  4. 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

!(a>max(a,b))!(b>max(a,b))(2.1)

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

(x>x)=false  i  (x>y)(y>z)=>(x>z)

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. !(a>b) i !(b>a).

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;
}

( Źródło: accumulate.cpp)

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:

  1. Sekwencje czyli pojemniki, w których kolejność elementów jest ustalana przez korzystającego z pojemnika (klienta) są to:
    1. vector
    2. deque
    3. list
  2. Kontenery asocjacyjne, czyli pojemniki, w których klient nie ma kontroli nad kolejnością elementów, są to:
    1. set
    2. map
    3. multiset
    4. multimap

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.

Rysunek 2.1. Hierarchia konceptów dla pojemników typu sekwencyjnego.

Rysunek 2.2. Hierarchia konceptów dla pojemników typu asocjacyjnego.

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; ;

( Źródło: invalid.cpp)

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.

Rysunek 2.3. Hierarchia konceptów dla iteratorów.

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).

Rysunek 2.4. Zakres.

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) {
 ...
}

( Źródło: accumulate.cpp)

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;} };

( Źródło: bind.cpp)

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));

( Źródło: bind.cpp)

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";
}

( Źródło: bind.cpp)

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; 
};

( Źródło: concept_check.cpp)

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
}

( Źródło: concept_check.cpp)

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);
}

( Źródło: concept_check.cpp)

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
}

( Źródło: concept_check.cpp)

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) {};
};

( Źródło: archeotype.cpp)

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();
};

( Źródło: archeotype.cpp)

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); }

( Źródło: archeotype.cpp)

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;}

( Źródło: archeotype.cpp)

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ą.