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

Z Studia Informatyczne
Przejdź do nawigacjiPrzejdź do wyszukiwania
Matiunreal (dyskusja | edycje)
Nie podano opisu zmian
 
Matiunreal (dyskusja | edycje)
Nie podano opisu zmian
Linia 48: Linia 48:


Drugim znakomitycm źródłem przykladów uogólnionego kodu jest
Drugim znakomitycm źródłem przykladów uogólnionego kodu jest
repozytorium bibliotek \cd{boost} (\url{<math>BOOST_HOME/doc/}). Stamtąd
repozytorium bibliotek {boost} ({<math>BOOST_HOME/doc/}). Stamtąd
tęż będę podawał przykłady i znów gorąco zachęcam państwa do
tęż będę podawał przykłady i znów gorąco zachęcam państwa do
zaglądania tam samemu.
zaglądania tam samemu.
Linia 364: Linia 364:


</math></center>
</math></center>
!( a>\max(a,b) )\; \wedge \; !(b>\max(a,b))
!( a>(a,b) )   !(b>(a,b))
<center><math>
<center><math>


Linia 383: Linia 383:
więc aby spełnione było
więc aby spełnione było


</math></center>\aligned(x>x) <nowiki>=</nowiki> false\quadi\quad
</math></center>(x>x) <nowiki>=</nowiki> falsei
(x>y) \wedge (y>z) \Rightarrow\; (x>z)
(x>y) (y>z) (x>z)
\endaligned<center><math>
<center><math>


To rozumowanie można by ciagnąć dalej i zauważyć że nawet z tym
To rozumowanie można by ciagnąć dalej i zauważyć że nawet z tym
Linia 463: Linia 463:
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
\cd{vector} i \cd{list} implementujące odpowiednio "inteligentną"
{vector} i {list} 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:
\beginlstlisting
 
std::vector<int> v(10);
std::vector<int> v(10);
v[8]<nowiki>=</nowiki>1;
v[8]<nowiki>=</nowiki>1;
\endlstlisting
 
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 wprzód czy w tył
\beginlstlisting
 
/*Uwaga! To nie jest kod STL-owy !!!*/
/*Uwaga! To nie jest kod STL-owy !!!*/
lista<int> l;
lista<int> l;
Linia 479: Linia 479:


l.current()<nowiki>=</nowiki>1; /* zwraca referencję do elementu bieżącego
l.current()<nowiki>=</nowiki>1; /* zwraca referencję do elementu bieżącego
\endlstlisting
 
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
Linia 493: Linia 493:
swojego początku i na swój koniec.  Korzystając z nich można listę
swojego początku i na swój koniec.  Korzystając z nich można listę
przeglądać następująco
przeglądać następująco
\beginlstlisting
 
std::list<int> l;
std::list<int> l;
/* tu jakoś inicjalizujemy liste*/
/* tu jakoś inicjalizujemy liste*/
Linia 502: Linia 502:
}
}
}
}
\endlstlisting
 
Przykładowy ogólny algorytm oparty o iteratory może wyglądać w ten sposób:
Przykładowy ogólny algorytm oparty o iteratory może wyglądać w ten sposób:
\beginlstlisting 
 
template <class InputIterator, class T>
template <class InputIterator, class T>
T accumulate(InputIterator first, InputIterator last, T init) {
T accumulate(InputIterator first, InputIterator last, T init) {
Linia 512: Linia 512:
return total;
return total;
}
}
/*\href{mod02/code/accumulate.cpp}{accumulate.cpp}*/
/*{mod02/code/accumulate.cpp}{accumulate.cpp}*/
\endlstlisting


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 wectora zezwala na konstrukcje
\cd{it[i]} a iterator listy już nie. Oznacza to że algorytm który
{it[i]} a iterator listy już nie. Oznacza to że algorytm który
działa dla iteratorów wektora (np. \cd{sort}) nie musi działać dla
działa dla iteratorów wektora (np. {sort}) 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
\cd{std::vector<T>::iterator} jest modelem konceptu bardziej
{std::vector<T>::iterator} jest modelem konceptu bardziej
wyspecjalizowanego niż koncept którego modelem jest
wyspecjalizowanego niż koncept którego modelem jest
\cd{std::list<T>::iterator}. Zobaczymy to w następnej części tego
{std::list<T>::iterator}. Zobaczymy to w następnej części tego
wykładu.
wykładu.


Linia 532: Linia 531:
ustalana przez korzystającego z pojemnika (klienta) są to:  
ustalana przez korzystającego z pojemnika (klienta) są to:  


#* \href{<math>SGI_STL_HOME/Vector.html}{\cd{vector}}
#* {<math>SGI_STL_HOME/Vector.html}{\cd{vector}}


#* \href{</math>SGI_STL_HOME/Deque.html}{\cd{deque}}  
#* \href{</math>SGI_STL_HOME/Deque.html}{{deque}}  


#* \href{<math>SGI_STL_HOME/List.html}{\cd{list}}
#* {<math>SGI_STL_HOME/List.html}{\cd{list}}


# Kontenery asocjacyjne czyli pojemniki w których klient nie ma
# Kontenery asocjacyjne czyli pojemniki w których klient nie ma
kontroli nad kolejnością elementów, są to:
kontroli nad kolejnością elementów, są to:


#* \href{</math>SGI_STL_HOME/set.html}{\cd{set}}
#* \href{</math>SGI_STL_HOME/set.html}{{set}}


#* \href{<math>SGI_STL_HOME/map.html}{\cd{map}}
#* {<math>SGI_STL_HOME/map.html}{\cd{map}}


#* \href{</math>SGI_STL_HOME/multiset.html}{\cd{multiset}}
#* \href{</math>SGI_STL_HOME/multiset.html}{{multiset}}


#* \href{<math>SGI_STL_HOME/multimap.html}{\cd{multimap}}
#* {<math>SGI_STL_HOME/multimap.html}{\cd{multimap}}


Ponadto różni dostawcy oferują dodatkowe pojemniki. Na uwagę zasługuje
Ponadto różni dostawcy oferują dodatkowe pojemniki. Na uwagę zasługuje
Linia 594: Linia 593:
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:
\beginlstlisting
 
std::vector<int>::iterator it;
std::vector<int>::iterator it;
int i;
int i;
Linia 606: Linia 605:
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);   
/*\parbox{12cm}{ ponieważ przekraczamy koniec wektora, kontener zaalokuje\
/*{12cm}{ ponieważ przekraczamy koniec wektora, kontener zaalokuje
\ dodatkową pamięc. Może się to wiązać z koniecznośćią, przeniesienia\
dodatkową pamięc. Może się to wiązać z koniecznośćią, przeniesienia
zawartości wektora v w inne miejsce pamięci. \
zawartości wektora v w inne miejsce pamięci.  
To spowoduje że wskaźnik it przestanie pokazywać na początek wektora v}
To spowoduje że wskaźnik it przestanie pokazywać na początek wektora v}
*/
*/
Linia 623: Linia 622:
std::cerr<<*it<<std::endl;
std::cerr<<*it<<std::endl;
;   
;   
/*\href{mod02/code/invalid.cpp}{invalid.cpp}*/
/*{mod02/code/invalid.cpp}{invalid.cpp}*/
\endlstlisting
 
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ę
\beginlstlisting
 
std::vector<int> buff(100);/* staramy się zająć pamięć za v*/
std::vector<int> buff(100);/* staramy się zająć pamięć za v*/
\endlstlisting 
 
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\footnote{Wbrew pozorom jest to najgorsza
program zadziała poprawnie{Wbrew pozorom jest to najgorsza
możliwa sytuacja}!
możliwa sytuacja}!


Linia 653: Linia 652:
rysunek&nbsp;[[##fig:iteratory|Uzupelnic fig:iteratory|]]. Zaznaczono na nim również które  
rysunek&nbsp;[[##fig:iteratory|Uzupelnic fig:iteratory|]]. Zaznaczono na nim również które  
koncepty kontenerów wymagają danego modelu iteratora.
koncepty kontenerów wymagają danego modelu iteratora.
\beginfigure [p]  
[p]  
\begincenter
 
\includegraphics[height<nowiki>=</nowiki>\textwidth,angle<nowiki>=</nowiki>90]{mod02/graphics/iteratory.eps}
[height<nowiki>=</nowiki>,angle<nowiki>=</nowiki>90]{mod02/graphics/iteratory.eps}
\endcenter
 
\caption{Hierarchia konceptów dla iteratorów}
{Hierarchia konceptów dla iteratorów}
\endfigure


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
Linia 672: Linia 670:
Iteratory pozwalają określanie zakresu elementów w kontenerze poprzez
Iteratory pozwalają 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 [\cd{it1},\cd{it2} ).   
końcem zakresu.  Zakres oznaczamy poprzez [{it1},{it2} ).   
\begincenter 
 
\includegraphics[width<nowiki>=</nowiki>12cm,bb <nowiki>=</nowiki> 100 250 700 450  ]{mod02/graphics/zakres.eps}
[width<nowiki>=</nowiki>12cm,bb <nowiki>=</nowiki> 100 250 700 450  ]{mod02/graphics/zakres.eps}
\endcenter
 
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.
\beginlstlisting
 
double x[10];
double x[10];
double *end<nowiki>=</nowiki>&x[10];
double *end<nowiki>=</nowiki>&x[10];
/* zwykłe wskażniki mogą być użyte jako iteratory */  
/* zwykłe wskażniki mogą być użyte jako iteratory */  
std::cout<<accumulate(x,end,0)<<endl; /* suma elementów tablicy*/
std::cout<<accumulate(x,end,0)<<endl; /* suma elementów tablicy*/
\endlstlisting
 
Każdy kontener posiada motody \cd{begin()} i \cd{end()} zwracające
Każdy kontener posiada motody {begin()} i {end()} 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:
wyglada więc następująco:
\beginlstlisting
 
typedef vector<int>::iterator iterator;
typedef vector<int>::iterator iterator;
vector<it> v(100);
vector<it> v(100);
Linia 693: Linia 691:
...
...
}
}
/*\href{mod02/code/accumulate.cpp}{accumulate.cp}*/
/*{mod02/code/accumulate.cpp}{accumulate.cp}*/
\endlstlisting
 
Proszę zwrócić uwagę na wykorzystanie operator \cd{\!<nowiki>=</nowiki>} do sprawdzenia
Proszę zwrócić uwagę na wykorzystanie operator {<nowiki>=</nowiki>} 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 \cd{operator<()}. Reszta jest tylko
porównywane za pomocą operatora {operator<()}. Reszta jest tylko
\cd{EqualityComparable}.
{EqualityComparable}.


===Algorytmy===
===Algorytmy===
Linia 710: Linia 708:


Oczywiście część algorytmów np.
Oczywiście część algorytmów np.
\cd{sort} wymaga bardziej wyrafinowanych iteratorów, nie dostarczanych
{sort} 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 718: Linia 716:
pojęcia fukcji, czyli coś na do czego można zastosować składnię
pojęcia fukcji, czyli coś na 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 \cd{operator()(...)} .
oraz obiekty klas w których zdefiniowano {operator()(...)} .


Funktory w STL są podzielone ze względu na liczę argumentów wywołania.
Funktory w STL są podzielone ze względu na liczę argumentów wywołania.
\cd{Generator} nie przyjmują żadnego argumentu, \cd{UnaryFunction}
{Generator} nie przyjmują żadnego argumentu, {UnaryFunction}
posiada jeden argument a \cd{BinaryFunction} dwa argumenty wywołania.
posiada jeden argument a {BinaryFunction} dwa argumenty wywołania.
Ważną podklasą są funkcje zwracające wartość typu \cd{bool} nazywane
Ważną podklasą są funkcje zwracające wartość typu {bool} nazywane
predykatami. Rozróżniamy więc  \cd{UnaryPredicate} i
predykatami. Rozróżniamy więc  {UnaryPredicate} i
\cd{BinaryPredicate}.
{BinaryPredicate}.


Ż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:
\beginlstlisting 
 
template<typename T> class SequenceGen {
template<typename T> class SequenceGen {
private:
private:
Linia 741: Linia 739:
T operator()() {T tmp<nowiki>=</nowiki>_start; _start+<nowiki>=</nowiki>_step; return tmp;}
T operator()() {T tmp<nowiki>=</nowiki>_start; _start+<nowiki>=</nowiki>_step; return tmp;}
};
};
/* \href{mod02/code/bind.cpp}{bind.cpp}*/
/* {mod02/code/bind.cpp}{bind.cpp}*/
\endlstlisting
 
Za pomocą obiektu klasy \cd{SequenceGen} możemy wypełnić wektor
Za pomocą obiektu klasy {SequenceGen} możemy wypełnić wektor
sekwencją 20 pierwszych nieparzystych liczb całkowitych:
sekwencją 20 pierwszych nieparzystych liczb całkowitych:
\beginlstlisting 
 
const size_t n <nowiki>=</nowiki> 20 ;
const size_t n <nowiki>=</nowiki> 20 ;
vector<int> v(n);
vector<int> v(n);
generate_n(v.begin(),n,SequenceGen<int>(1,2));
generate_n(v.begin(),n,SequenceGen<int>(1,2));
/* \href{mod02/code/bind.cpp}{bind.cpp}*/
/* {mod02/code/bind.cpp}{bind.cpp}*/
\endlstlisting
 
Standardowy algorytm
Standardowy algorytm
\beginlstlisting
 
template <class OutputIterator, class Size, class Generator>
template <class OutputIterator, class Size, class Generator>
OutputIterator generate_n(OutputIterator first,  
OutputIterator generate_n(OutputIterator first,  
Size n, Generator gen);
Size n, Generator gen);
\endlstlisting
 
służy właśnie do wypełniania kontenerów za pomocą \cd{n} kolejnych
służy właśnie do wypełniania kontenerów za pomocą {n} kolejnych
wyników wywołania funktora \cd{gen}.  Powyższy kod ilustruje typowy
wyników wywołania funktora {gen}.  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ć.   
Linia 765: Linia 763:
poszukamy pierwszego elementu większego od czterech (powinno to być
poszukamy pierwszego elementu większego od czterech (powinno to być
pięć). Służy do tego algorytm  
pięć). Służy do tego algorytm  
\beginlstlisting 
 
template<class InputIterator, class Predicate>
template<class InputIterator, class Predicate>
InputIterator find_if(InputIterator first,  
InputIterator find_if(InputIterator first,  
InputIterator last,
InputIterator last,
Predicate pred);
Predicate pred);
\endlstlisting
 
Który przeszukuje zakres \cd{[first,last)} do napotkania pierwszego
Który przeszukuje zakres {[first,last)} do napotkania pierwszego
elementu dla którego predykat \cd{pred} jest prawdziwy i zwraca
elementu dla którego predykat {pred} 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
\cd{find_if} zwraca \cd{last}. Do zakończenia programu potrzebujemy
{find_if} zwraca {last}. 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 adapteru funkcji
\cd{bind2nd}.  Ten obiekt przyjmuje funktor dwuargumentowy
{bind2nd}.  Ten obiekt przyjmuje funktor dwuargumentowy
(\cd{AdaptableBinaryFunction}) \cd{F(T,U)} i jakąś wartość \cd{x} typu
({AdaptableBinaryFunction}) {F(T,U)} i jakąś wartość {x} typu
\cd{U} i zwraca funktor jednoparametrowy \cd{F(T,x)}. Korzystając z
{U} i zwraca funktor jednoparametrowy {F(T,x)}. Korzystając z
predefiniowanego predykatu \cd{greater} możemy napisać:
predefiniowanego predykatu {greater} możemy napisać:
\beginlstlisting 
 
vector<int>::iterator it<nowiki>=</nowiki> find_if(v.begin(),v.end(),
vector<int>::iterator it<nowiki>=</nowiki> find_if(v.begin(),v.end(),
bind2nd(greater<int>(),4));
bind2nd(greater<int>(),4));
Linia 789: Linia 787:
cout<<"nie znaleziono zadanego elementy";
cout<<"nie znaleziono zadanego elementy";
}
}
/* \href{mod02/code/bind.cpp}{bind.cpp}*/
/* {mod02/code/bind.cpp}{bind.cpp}*/
\endlstlisting
 
STL wprowadza więc do C++ elementy programowanie funkcyjnego.  
STL wprowadza więc do C++ elementy programowanie funkcyjnego.  


Linia 805: Linia 803:


Oczywiście kompilator podczas konkretyzacji szablonu sprawdza
Oczywiście kompilator podczas konkretyzacji szablonu sprawdza
syntaktyczną zgodność przeka\-zanego 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
Linia 818: Linia 816:


Idea tworzenia takich szablonów jest prosta (zob.
Idea tworzenia takich szablonów jest prosta (zob.
\url{<math>BOOST_LIBS/concept_check/concept_check.htm}: dla każdego
{<math>BOOST_LIBS/concept_check/concept_check.htm}: dla każdego
konceptu tworzymy szablon zawierający funkcję \cd{constraints()} która
konceptu tworzymy szablon zawierający funkcję \cd{constraints()} która
zawiera wszystkie możliwe poprawne wyrażenia dla danego konceptu. Np.
zawiera wszystkie możliwe poprawne wyrażenia dla danego konceptu. Np.
Linia 907: Linia 905:
STL\url{</math>BOOST_LIBS/concept_check/concept_check.htm}. Hierachia którą
STL\url{</math>BOOST_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 \url{<math>SGI_STL_HOME}. Główna
zaprezentowałem i która jest opisana w {<math>SGI_STL_HOME}. 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 (\cd{MutableContainer}) i tych
umożliwiaja modyfikację swoich elementów (\cd{MutableContainer}) i tych
Linia 922: Linia 920:
dokładnie implementują interfejs danego konceptu.  Opierając się na
dokładnie implementują interfejs danego konceptu.  Opierając się na
\url{</math>BOOST_LIBS/concept_check/concept_check.htm} przedstawię teraz
\url{</math>BOOST_LIBS/concept_check/concept_check.htm} przedstawię teraz
implementację archeotypu dla konceptu \cd{Comparable}.  Koncept
implementację archeotypu dla konceptu {Comparable}.  Koncept
\cd{Comparable} nie wymaga posiadana konstruktora defaultowego,
{Comparable} nie wymaga posiadana konstruktora defaultowego,
konstruktora kopiujacego oraz operatora przypisania dlatego w naszym
konstruktora kopiujacego oraz operatora przypisania dlatego w naszym
archeotypie zdefiniujemy je jako prywatne:
archeotypie zdefiniujemy je jako prywatne:
\beginlstlisting
 
class comparable_archetype {
class comparable_archetype {
private:
private:
Linia 936: Linia 934:
comparable_archetype(dummy_argument) {};
comparable_archetype(dummy_argument) {};
};
};
/*\href{mod02/code/archeotype.cpp}{archeotype.cpp}*/
/*{mod02/code/archeotype.cpp}{archeotype.cpp}*/
\endlstlisting
 
Aby móc tworzyć obiekty typu \cd{comparable_archetype} dodaliśmy
Aby móc tworzyć obiekty typu {comparable_archetype} dodaliśmy
niestandardowy konstruktor z argumentem sztucznego typu:
niestandardowy konstruktor z argumentem sztucznego typu:
\beginlstlisting 
 
class dummy_argument {};
class dummy_argument {};
\endlstlisting
 
używanego tylko na tą okazję (jego nazwa powinna być unikatowa).
używanego tylko na tą okazję (jego nazwa powinna być unikatowa).


Operator \cd{operator<()} nie musi zwracać wartości typu \cd{bool}a
Operator {operator<()} nie musi zwracać wartości typu {bool}a
jedynie wartość typu konwertowalnegona \cd{bool}, dlatego tworzymy taki typ:
jedynie wartość typu konwertowalnegona {bool}, dlatego tworzymy taki typ:
\beginlstlisting 
 
struct boolean_archetype  {
struct boolean_archetype  {
operator const bool() const {return true;}
operator const bool() const {return true;}
};
};
\endlstlisting
 
i podajemy go jako typ zwracany przez operatory porównania
i podajemy go jako typ zwracany przez operatory porównania
\beginlstlisting
 
boolean_archetype operator<(const comparable_archetype &,
boolean_archetype operator<(const comparable_archetype &,
    const comparable_archetype &){
    const comparable_archetype &){
Linia 962: Linia 960:
return boolean_archetype();
return boolean_archetype();
};
};
/*\href{mod02/code/archeotype.cpp}{archeotype.cpp}*/
/*{mod02/code/archeotype.cpp}{archeotype.cpp}*/
\endlstlisting
 
Teraz możemy już przetestować nasz szablon {max}.


Teraz możemy już przetestować nasz szablon \cd{max}.
\beginlstlisting 
template<typename T>  
template<typename T>  
const T &max(const T &a,const T &b) {return (a>b)?a:b;}
const T &max(const T &a,const T &b) {return (a>b)?a:b;}
Linia 973: Linia 970:
comparable_archetype ca(dummy_argument());  
comparable_archetype ca(dummy_argument());  
max(ca,ca);  
max(ca,ca);  
/*\href{mod02/code/archeotype.cpp}{archeotype.cpp}*/
/*{mod02/code/archeotype.cpp}{archeotype.cpp}*/
}
}
\endlstlisting
 
Poprawna kompilacja tego kodu przekonuje nas że koncept
Poprawna kompilacja tego kodu przekonuje nas że koncept
\cd{Comparable} jest wystarczajacy, przynajmniej syntaktycznie. Proszę
{Comparable} 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
\beginlstlisting 
 
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;}
/*\href{mod02/code/archeotype.cpp}{archeotype.cpp}*/
/*{mod02/code/archeotype.cpp}{archeotype.cpp}*/
\endlstlisting
 
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 \cd{boost}
Implementacja archeotypów w biblitece {boost}
\url{<math>BOOST_LIBS/concept_check/concept_check.htm} zezwala na takie
{<math>BOOST_LIBS/concept_check/concept_check.htm} zezwala na takie
konstrukcje i gorąco zachęcam do zapoznania się z nia.
konstrukcje i gorąco zachęcam do zapoznania się z nia.


==Podsumowanie==</math>
==Podsumowanie==</math>

Wersja z 11:19, 17 sie 2006

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ństo już widzą 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. Tka jaki w przypadku tworzenie 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 gra podobną rolę jak interfejs w programowaniu uogólnionym, ale przynależność do tej rodziny jest określona proceduralnie: do konceptu nalężą typu które spełniają pewne wymagania. Czyli jeśli coś kwacze jak kaczka to to jest 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 nauczanie 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 wykladzie, oraz wykonywanie podanych ćwiczeń.

Drugim znakomitycm źródłem przykladów uogólnionego kodu jest

repozytorium bibliotek {boost} ({Parser nie mógł rozpoznać (błąd składni): {\displaystyle BOOST_HOME/doc/}). Stamtąd tęż 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 dana 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śc 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 \cd{shape_table}: \beginlstlisting for(size_t i=0;i<n;++i) shape_table[i]->draw(); /*\href{./mod02/code/shape.html}{kod źródłowy}*/ \endlstlisting Programista piszący ten kod nie musi wiedziec jakiego typu kształt jest przechowywany w danym elemencie tablice \cd{shape_table} i jak jest zaimplementowana funkcja \cd{draw}. Istotne jest że każdy obiekt którego wkaźnik przechowywany jest w tej tablicy posiadał metodę \cd{draw}. Innymi słowy programista korzysta tu tylko ze znajomości i dostępności interfejsu obiektów typy 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: \beginlstlisting 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() {}; /*\href{./mod02/code/shape.html}{kod źródłowy}*/ }; \endlstlisting Klasa ta stanowić będzie klasę bazową dla wszystkich klas opisujących kształty. Klasa \cd{Shape} jest klasą abstrakcyjną ponieważ zawiera nie zaimplementowaną wirtualną czystą fukcję \cd{void draw()}. Kod definiujący konkretne klasy kształtów może wygladać następująco: \beginlstlisting 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;}; }; /*\href{./mod02/code/shape.html}{kod źródłowy}*/ \endlstlisting i \beginlstlisting 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;}; }; /*\href{./mod02/code/shape.html}{kod źródłowy}*/ \endlstlisting Teraz możemy zdefiniować już funkcję odświeżającą ekran: \beginlstlisting void draw_shapes(Shape *table[],size_t n) { for(size_t i=0;i<n;++i) table[i]->draw(); } /*\href{./mod02/code/shape.html}{kod źródłowy}*/ \endlstlisting Funkcja \cd{draw_shapes} wykorzystuje zachowanie polimorficzne: to która funkcja \cd{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 \beginlstlisting 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); } /*\href{./mod02/code/shape.html}{kod źródłowy}*/ \endlstlisting 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== Patrzac na kod funkcji \cd{draw_shapes} możemy zauważyć że korzysta on jedynie z własności posiadania przez wskazywane obiekty metody \cd{draw()}. To sygnatura czyli typ parametru wywołania tej funkcji określa że musi to być wskaźnik na typ \cd{Shape}. Z poprzedniego wykładu pamiętamy że możemy zrezygnować z wymuszania typu argumentu wywołania funkcji poprzez użycie szablonu funcji: \beginlstlisting template<typename T> void draw_template(T table[],size_t n) { for(size_t i=0;i<n;++i) table[i].draw(); } /*\href{./mod02/code/shape_template.html}{kod źródłowy}*/ \endlstlisting taką funkcję możemy wywołać dla dowolnej tablicy, byle tylko przechowywany typ posiadał metodę \cd{draw}. Mogą to być obiekty typów \cd{Circle} i \cd{Rectangle} (nie \cd{Shape}, obiekty klasy \cd{Shape} nie istnieją!), ale też inne zupełnie z nimi nie związane. Ilustruje to poniższy przykład \beginlstlisting 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); } /*\href{./mod02/code/shape_template.html}{kod źródłowy}*/ \endlstlisting 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ę \cd{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 na 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ę zbrane głowne właściwości każdego podejścia. # Dziedzieczenie i funkcje wirtualne ## Umożliwia pracę ze zbiorami niejednorodnych obiektów i korzysta z polimorfizmu dynamicznego. ## Wymaga wspólnej hierarchi dziedziczenia. ## Wymusza korzystanie ze wskaźników lub referencji i funkcji wirtualnych. ## Zazwyczaj generuje mniejszy kod. # Szablony ## Implementuje polimorfizm statyczny. ## Bezpiecznie obsługuje jednorodne zbiory obiektów ## Nie trzeba korzystać ze wskaźników i referencji ani funkcji wirtualnych. ## Nie musimy korzystać ze wspólnej hierarchii dziedziczenia. ==Koncepty== Przyjrzyjmy się jeszcze raz deklaracji funkcji \cd{draw_shapes} i \cd{draw_template}: Kiedy programista widzi deklarację \beginlstlisting void draw_shapes(Shape *table[]) \endlstlisting wie że interfejs wykorzystywany przez funkcję \cd{draw} jest zdefiniowany przez klasę \cd{Shape}. Aby go poznać musi przeczytać kod i dokumentację tej klasy. Natomiast kiedy programista widzi deklarację \beginlstlisting template<typename T> void draw_template(T table[],size_t n); \endlstlisting to musi przesledzić kod funkcji \cd{draw_templates} aby poznać ograniczenia nałożone na argument szablonu \cd{T}. W tym przypadku nie jest to trudne, ale ogólnie może to być zadanie nietrywialne. 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 hierarachi dziedziecznia. Mówimy że koncept \cd{A} jest bardziej wyspecjalizowany niż \cd{B} (\cd{A} is-refinement-of \cd{B}) jeśli zestaw ograniczeń konceptu \cd{B} zawiera się w zestwie ograniczeń konceptu \cd{A}. Będę też używał określenia \cd{A} jest "uszczegółowieniem" \c{B}. Pojęcie konceptu gra więc przy programowaniu za pomocą szablonów podobną rolę jak pojęcie interfejsu przy programowaniu za pomocą abstrakcyjnych klas bazowych i polimorfizmu dynamicznego. W przeciwieństwie do interfejsu jest to jednak pojęcie bardziej ``ulotne'' bo nie narzucamy go za pomocą formalnej definicji klasy abstrakcyjnej. Koncepty definiujemy poprzez mniej lub bardziej ścisłe wypisanie nakładanych przez nie ograniczeń. Ograniczenia te mogą zawierać między innymi: # Prawidłowe wyrażenia. Zestaw wyrażeń języka C++ które muszą się poprawnie kompilować # Typy stowarzyszone. Ewentualne dodatkowe typy występujące w prawidłowych wyrażeniach. # Semantyka: zanczenie wyrażeń. Jednym ze sposobów określanie semantyki jest podawanie niezmienników, czyli wyrażeń które dla danego konceptu są zawsze prawdziwe. # Złożoność algorytmów. Gwarancje co do czasu i innych zasobów potrzebnych do wykonania danego wyrażenia. Programowanie uogólnione polega więc na wyszukiwaniu konceptów, na tylke 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 \cd{max} z poprzedniego wykładu \beginlstlisting template<typename T> max(T a,T b) {return (a>b)?a:b;} \endlstlisting i zastanówmy się jakie koncepty możemy odkryć w tak prostym kodzie. Zacznijmy od gramatyki. Jakie warunki musi spełniać typ \cd{T} aby podstawienie go jako argument szablonu \cd{max} dawało poprawne wyrażenie? Oczywistym warunkiem jest że dla tego typu musi być zdefiniowany operator porównania \cd{bool operator>(...)}. Specjalnie nie wyspecyfikowałem sygnatury tego operatora. Nie ma np. znaczenia jak parametry są przekazywane, co więcej \cd{operator>(...)} może być zadefiniowany jako składowa klasy i posiadać tylko jeden jawny argument. Ważne jest to że jeśli \cd{x} i \cd{y} są obiektami typu \cd{T} to wyrażenie: \beginlstlisting x>y \endlstlisting jest poprawne (skompiluje się). Łatwiej jest przeoczyć fakt że ponieważ argumenty wywołania są zwracane i przekazywane przez wartość to typ \cd{T} musi posiadać konstruktor kopiujący. Oznacza to że jeśli \cd{x} i \cd{y} są obiektami typu \cd{T} to wyrażenia: \beginlstlisting [caption="",label=ex:copy-constr] T(x); T x(y); T x = y; \endlstlisting są poprawne. Spełnienie obudwu tych warunków zapewni nam poprawność gramatyczną wywołania szablonu z danym typem, tzn. kod się skompiluje. A co z poprawnośćią semantyczna? Mogłoby sie wydawać że jest bez znaczenia jak zdefiniujemy \cd{operator>(...)}. Koncept typu \cd{T} jest jednak częścią kontraktu dla funkcji \cd{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 a 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 \cd{max} nie może być większy od wyniku, czyli wyrażenie }

!( a>(a,b) )  !(b>(a,b))

Parser nie mógł rozpoznać (błąd składni): {\displaystyle musi być zawsze prawdziwe. Jasne jest że jeśli dla jakiegoś typu \cd{X} zdefiniujemy operator porównania tak aby zwracał zawsze prawdę \beginlstlisting bool operator>(const X &a,const X &b) {return 1;} \endlstlisting lub aby był równoważny operatorowi równośći: \beginlstlisting bool operator>(const X &a,const X &b) {return a==b;} \endlstlisting to wyrażenie [[##eq:max-post|Uzupelnic eq:max-post|]] nie może być prawdziwe dla żadnej wartości \cd{a} i \cd{b}. Aby funkcja \cd{max} mogła spełnić swój warunek końcowy musimy narzycić pewne ograniczenia semantyczne na \cd{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) = falsei

(x>y) (y>z) (x>z)

Parser nie mógł rozpoznać (błąd składni): {\displaystyle To rozumowanie można by ciagnąć dalej i zauważyć że nawet z tym ograniczeniem uzyskamy nieintuicyjne wyniki w przypadku gdy obiekty \cd{a} i \cd{b} będą nieporównywalne tzn. } !(a>b)i!(b>a)Parser nie mógł rozpoznać (błąd składni): {\displaystyle . Poprawność semantyczną konstruktora kopiującego jest trudniej zdefiniować, ograniczymy się więc tylko do stwierdzenia że wykonanie operacji [[##ex:copy-constr|Uzupelnic ex:copy-constr|]] powoduje powstanie kopii obiektu \cd{x} (cokolwiekby to nie znaczyło). ===Comparable i Assignable=== Zbierając to wszystko do kupy, dostajemy zbiór warunków który musi spełniać typ \cd{T} aby móc go podstawić do szablonu funkcji \cd{max}. Czy to oznacza że zdefiniowaliśmy już poprawny koncept? Żeby się o tym przekonać spróbujmy go nazwać. Narzuca się nazwa w stylu \cd{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 posiadanie konstruktora kopiujacego zmieniając deklarację \cd{max} na: \beginlstlisting template<typename T> const T& max(const T&,const T&); \endlstlisting 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 operator < poprzez: \beginlstlisting bool operator<(const T& a,const T&b) {return b>a;}; \endlstlisting Podobnie istnienie konstruktora kopiującego jest blisko związane z istnieniem operatora przypisania. Tak więc dochodzimy do dwu konceptów: \cd{Comparable} reprezentującego typy których obiekty można porównywać za pomocą operatorów \cd{<} i \cd{>} (zob. rys.), oraz \cd{Assignable} reprezentujacego typy których obiekty możemy kopiować i przypisywać do siebie (zob. rys.). Taką zabawę można kontynuować, pytając np. co z operatorem porównania \cd{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łączyny do jednego konceptu czy nie decyduje np. odpwiedź 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 przedtawione są na stronach firmy SGI \url{} SGI_STL_HOME}

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++ dadatkowe funkcje, lub jako na zbiór konceptów stanowiących podstawę do projetowania programów uogólnionych. Ja chciałbym podkreślić 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 wprzód czy w tył

/*Uwaga! To nie jest kod STL-owy !!!*/ lista<int> l; l.reset(); /* ustwia 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; } /*{mod02/code/accumulate.cpp}{accumulate.cpp}*/

Oczywiście nie da się zignorować fundamentalnych różnic pomiędzy listą a wektorem. Dlatego np. iterator wectora 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:

    • {Parser nie mógł rozpoznać (nieznana funkcja „\cd”): {\displaystyle SGI_STL_HOME/Vector.html}{\cd{vector}} #* \href{} SGI_STL_HOME/Deque.html}Szablon:Deque
    • {Parser nie mógł rozpoznać (nieznana funkcja „\cd”): {\displaystyle SGI_STL_HOME/List.html}{\cd{list}} # Kontenery asocjacyjne czyli pojemniki w których klient nie ma kontroli nad kolejnością elementów, są to: #* \href{} SGI_STL_HOME/set.html}Szablon:Set
    • {Parser nie mógł rozpoznać (nieznana funkcja „\cd”): {\displaystyle SGI_STL_HOME/map.html}{\cd{map}} #* \href{} SGI_STL_HOME/multiset.html}Szablon:Multiset
    • {Parser nie mógł rozpoznać (nieznana funkcja „\cd”): {\displaystyle SGI_STL_HOME/multimap.html}{\cd{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 \cd{slist} oraz tablice haszujące \cd{hash_set} czy \cd{hash_map}\cite{sgi}. Hierachie konceptów kontenerów typu sekwencji przedstawia rysunek~[[##fig:sequence|Uzupelnic fig:sequence|]], a kontenerów asocjacyjnych rysunek~[[##fig:associate|Uzupelnic fig:associate|]]. \beginfigure [p] \begincenter \includegraphics[height=\textwidth,angle=90]{mod02/graphics/sequence.eps} \endcenter \caption{Hierarchia konceptów dla pojemników typu sekwencyjnego} \endfigure \beginfigure [p] \begincenter \includegraphics[height=\textwidth,angle=90]{mod02/graphics/associate.eps} \endcenter \caption{Hierarchia konceptów dla pojemników typu asocjacyjnego} \endfigure Nie będę tu omawiał tych wszystkich konceptów. Ich szczególowe opisy znajdują się na stronie \url{} SGI_STL_HOME}. Prowadzą do niej linki z

rysunków oraz z tej strony. Tutaj chciałbym tylko dodać parę luźnych komentarzy.

Po pierwsze rodzi się pytanie czy taka skomplikowana taxonomia jest 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ą w zasadzie wystarczy zaznajomić się z opisami kontenerów i hierachią iteratorów (rysunek Uzupelnic fig:iteratory|). 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 komponetami 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); /*{12cm}{ ponieważ przekraczamy koniec wektora, kontener zaalokuje dodatkową pamięc. 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;

/*{mod02/code/invalid.cpp}{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 operacja indeksowania wektora jest gwarantowana być rzędu O(1). Natomiast operacja dodania elementu w środku wektora jest rzędu O(N). Z listą jest odwrotnie i dlatego listy w STL nie posiadaja 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 Uzupelnic fig:iteratory|. Zaznaczono na nim również które koncepty kontenerów wymagają danego modelu iteratora. [p]

[height=,angle=90]{mod02/graphics/iteratory.eps}

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

[width=12cm,bb = 100 250 700 450 ]{mod02/graphics/zakres.eps}

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; /* suma elementów tablicy*/

Każdy kontener posiada motody {begin()} i {end()} zwracające iterator na początek i "poza koniec". Typowa pętla obsługi kontenera wyglada więc następująco:

typedef vector<int>::iterator iterator; vector<it> v(100); for(iterator it=v.begin();it!=v.end();++it) { ... } /*{mod02/code/accumulate.cpp}{accumulate.cp}*/

Proszę zwrócić uwagę na wykorzystanie operator {=} 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 algorytmu 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ś na 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 liczę argumentów wywołania. {Generator} nie przyjmują ż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;} }; /* {mod02/code/bind.cpp}{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)); /* {mod02/code/bind.cpp}{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 adapteru funkcji {bind2nd}. Ten obiekt 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 elementy"; } /* {mod02/code/bind.cpp}{bind.cpp}*/

STL wprowadza więc do C++ elementy programowanie 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 mechanismu 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 definiowanym przez ten szablon konceptem. Niestety można w ten sposób sprawdzać tylko zgodność syntaktyczną.

Idea tworzenia takich szablonów jest prosta (zob. {Parser nie mógł rozpoznać (błąd składni): {\displaystyle BOOST_LIBS/concept_check/concept_check.htm}: dla każdego konceptu tworzymy szablon zawierający funkcję \cd{constraints()} która zawiera wszystkie możliwe poprawne wyrażenia dla danego konceptu. Np. dla konceptu \cd{Comparable} możemy zdefiniować: \beginlstlisting template<typename T> struct ComparableConcept { void constraints() { require_boolean_expr( a > b); require_boolean_expr( a < b); }; T a,b; }; /* \href{mod02/code/concept_check.cpp}{concept\_check.cpp}*/ \endlstlisting szablon \cd{require_boolean_expr} \beginlstlisting template <class TT> void require_boolean_expr(const TT& t) { bool x = t; ignore_unused_variable_warning(x); /*używa zmiennej {\tt x} aby kompilator nie generował ostrzeżenia */ } /* \href{mod02/code/concept_check.cpp}{concept\_check.cpp}*/ \endlstlisting sprawdza czy jego argument, a więc wartość zwracana przez operatory, może być konwertowana na \cd{bool}. Zwracam uwagę że nie możemy w kodzie szablony \cd{Comparable} użyć defaultowego konstruktora, bo nie jest on wymagany. Dlatego zmienne \cd{a} i \cd{b} nie były zdefiniowane wewnątrz funkcji \cd{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 wykonać funkcję \linebreak\cd{ComparableConcept<T>::constraints()}. Możemy tego dokonać pobierając adres funkcji i przypisując go wskaźnika. Kompilator skompiluje kod funkcji, ale jej nie wykona. Dodatkowo najprawdopodobniej kompilator optymalizujący usunie to przypisanie jako nie używany kod, ale dopiero po kompilacji (no chyba że jest bardzo ale to bardzo inteligentny). Dla wygody opakujemy tą konstrukcję w szablon funkcji: \beginlstlisting template <class Concept> inline void function_requires() { void (Concept::*x)() = &Concept::constraints; ignore_unused_variable_warning(x); } /* \href{mod02/code/concept_check.cpp}{concept\_check.cpp}*/ \endlstlisting Możemy teraz używać szablony \cd{Comparable} w następujacy sposób: \beginlstlisting main() { function_requires<ComparableConcept<int> >(); function_requires<ComparableConcept<std::complex> >();/*bład*/ } /* \href{mod02/code/concept_check.cpp}{concept\_check.cpp}*/ \endlstlisting Bardziej skomplikowane koncepty możemy sprawdzać korzystając z klas sprawdzających dla innych konceptów np: \beginlstlisting 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 {\tt 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; }; \endlstlisting Biblioteka \cd{boost} skąd wzięty został ten przykłąd posiada implementację szablonów dla każdego konceptu z STL\url{} BOOST_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 {Parser nie mógł rozpoznać (błąd składni): {\displaystyle SGI_STL_HOME}. Główna różnica to wprowadzenie rozróżnienia pomiędzy kontenerami które umożliwiaja modyfikację swoich elementów (\cd{MutableContainer}) i tych które na to nie pozwalają (\cd{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 \url{} BOOST_LIBS/concept_check/concept_check.htm} przedstawię teraz implementację archeotypu dla konceptu {Comparable}. Koncept {Comparable} nie wymaga posiadana 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) {}; }; /*{mod02/code/archeotype.cpp}{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 konwertowalnegona {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(); }; /*{mod02/code/archeotype.cpp}{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); /*{mod02/code/archeotype.cpp}{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;} /*{mod02/code/archeotype.cpp}{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} {Parser nie mógł rozpoznać (błąd składni): {\displaystyle BOOST_LIBS/concept_check/concept_check.htm} zezwala na takie konstrukcje i gorąco zachęcam do zapoznania się z nia. ==Podsumowanie==}