Programowanie Obiektowe (język C++) Wykład 10. FUNKCJE WZORCOWE Funkcje wzorcowe wprowadzenie (1) Funkcje wzorcowe wprowadzenie (2) int max ( int a, int b ) return a>b? a : b; Aby mieć analogiczną funkcję działającą na danych typu double w jęz. C musimy wprowadzić dla niej odrębny identyfikator: double d_max ( double a, double b ) return a>b? a : b; W jęz. C++ mamy moŝliwość przeciąŝania identyfikatorów funkcji, więc moŝemy uŝyć tej samej nazwy: double max ( double a, double b ) return a>b? a : b; W obu językach C i C++ moŝemy się posłuŝyć w tej sytuacji, jako pewnego rodzaju alternatywą, tzw. makrodefinicją: #define MAX(a,b) (((a)>(b))? (a) : (b)) co daje nam moŝliwość posługiwania się wyraŝeniami przypominającymi wywołania 2-argumentowej funkcji o tej samej nazwie dla argumentów róŝnych typów: int i, j, k; double A, B, C;. k = MAX(2*i+5, j+i); C = MAX(3.4+A, 7*B); Ale wiąŝe się to z szeregiem niedogodności i pułapek. -1- -2- -3- -4-
Funkcje wzorcowe (1) W języku C++ moŝemy się posłuŝyć konstrukcją wzorca (szablonu) funkcji: template < class TYPE > TYPE max (TYPE a, TYPE b ) return a>b? a : b; gdzie TYPE moŝe być typem wbudowanym lub definiowanym w programie. <class TYPE> nazywa się tu opisem parametru wzorca. Mając tak zdefiniowany szablon, moŝe go wykorzystać do konkretyzacji funkcji przyjmujących argumenty potrzebnych typów, n.p.: int main ( ) int i, j, k; double A, B, C; k = max( i, j + 5 ); // skonkretyzuje int max ( int, int ); C = max( A, B + 0.5 ); // skonkretyzuje double max ( double, double ); A = max ( B, 10 ); // BŁĄD! konieczna ścisła zgodność typów Funkcje wzorcowe (2) 1. Wzorzec funkcji moŝe mieć więcej parametrów (ale nie mniej niŝ jeden!). 2. KaŜdy opis parametru wzorca składa się ze słowa kluczowego class ( lub typename ) oraz wybranego identyfikatora ( nazwy parametru ). 3. Na liście parametrów wzorca kaŝdy identyfikator moŝe wystąpić tylko raz. 4. Parametr wzorca staje się specyfikatorem typu, którego moŝna uŝywać w pozostałej części definicji funkcji wzorcowej ( n.p. w deklaracjach zmiennych lokalnych, operacjach rzutowania e.t.c. ) 5. KaŜdy parametr wzorca musi wystąpić co najmniej jeden raz w sygnaturze funkcji wzorcowej. Wychodząc od definicji funkcji: Funkcje wzorcowe (3) double Summa ( double tab[ ], int size ) double sum = 0; for ( int i = 0; i < size; i++ ) sum += tab[ i ]; return sum; MoŜemy łatwo utworzyć jednoparametrowy wzorzec: template < class T > T Summa ( T tab[ ], int size ) T sum = 0; for ( int i = 0; i < size; i++ ) sum += tab[ i ]; return sum; Funkcje wzorcowe (4) Wychodząc od definicji tej samej funkcji: double Summa ( double tab[ ], int size ) double sum = 0; for ( int i = 0; i < size; i++ ) sum += tab[ i ]; return sum; MoŜemy równie łatwo utworzyć wzorzec dwuparametrowy: template < class T, class S > T Summa ( T tab[ ], S size ) T sum = 0; for ( S i = 0; i < size; i++ ) sum += tab[ i ]; return sum; -5- -6- -7- -8-
Funkcje wzorcowe (5) RozróŜnianie przeciąŝonych funkcji wzorca i innych funkcji o tej samej nazwie jest realizowane wg schematu: 1. JeŜeli istnieje funkcja o deskryptorze dokładnie pasującym do wywołania, to ją wywołaj. 2. JeŜeli istnieje wzorzec pozwalający zrealizować (skonkretyzować) funkcję o dokładnie pasującym deskryptorze, to uŝyj tego wzorca. 3. JeŜeli istnieje funkcja przeciąŝona, którą moŝna dopasować wg zwykłych reguł (tzn. z zastosowaniem konwersji), to jej uŝyj. 4. JeŜeli Ŝadnego z punktów 1., 2., 3. nie dało się zastosować, to wywołanie zostanie uznane za błędne. Rozpatrzmy przykład: template < class T > T max ( T a, T b ) return a>b? a : b; void test ( ) int a, b; char c, d; Funkcje wzorcowe (6) //int A = max( a, c ); // BŁĄD! nie moŝna wygenerować int max(int,char); int B = max( a, b ); // int max(int, int); - niejawne utworzenie egzemplarza char C = max( c, d ); // char max(char,char); niejawne utworzenie egz. int D = max( c, d ); // char max(char, char); typ zwracanej wartości nie // nie naleŝy do deskryptora Funkcje wzorcowe (7) Funkcje wzorcowe (8) Ale: template < class T > T max ( T a, T b ) return a>b? a : b; int max ( int, int ); void test ( ) int a, b; char c, d; // deklaracja funkcji (być moŝe zewnętrznej) int A = max( a, c ); // O.K.! uŝyte będzie int max(int,int); // zgodnie z regułą 3. Inne moŝliwości: template < class T > T max ( T a, T b ) return a>b? a : b; template int max ( int, int ); template< > char max ( char a, char b ) return a>b? a : 0; // wymuszone (jawne) utworzenie egzemplarza // (tylko w zasięgu definicji szablonu) // dostarczenie szczególnej definicji egzemplarza! // (tylko w zasięgu definicji szablonu) -9- -10- -11- -12-
Klasy wzorcowe - wprowadzenie (1) W język C++ moŝemy się posłuŝyć równieŝ konstrukcją wzorca (szablonu) klasy. Podobnie jak w przypadku wzorców funkcji najprościej jest przyjąć za punkt wyjścia jakąś konkretną klasę (dobrze wcześniej sprawdzoną w praktyce). #define CSTACKSIZE 100 WZORCE KLAS ( SZABLONY KLAS ) class CharStack char tab [ CSTACKSIZE ]; int size, top; CharStack ( ) size = CSTACKSIZE; top = 0; void Push ( char e ) tab [ top++ ] = e; char Pop ( ) return tab [ --top ]; char Top ( ) return tab [ top - 1 ]; int Size ( ) const return size; int Used ( ) const return top; int Place ( ) const return size - top; void Display ( ) const cout << endl; for ( int i = 0; i < top; ++i ) cout << tab [ i ] << " "; Przy okazji (1) ZauwaŜmy, Ŝe dotychczas w definicji klasy podawaliśmy zwykle jedynie deklaracje metod. Ich definicje umieszczaliśmy na zewnątrz, najczęściej w tzw. pliku implementacyjnym. Tym razem definicje metod zostały podane od razu w definicji klasy. Jaka róŝnica? 1. Metoda definiowana w ciele definicji klasy otrzymuje domyślnie atrybut inline. Metody (i zwykłe funcje) z takim atrybutem nie mają jednokrotnie wygenerowanego kodu o określonym adresie, który jest uruchamiany przy kaŝdym odwołaniu do metody (funkcji). Zamiast tego kompilator moŝe (ale nie musi!) generować kod metody (funkcji) w kaŝdym miejscu jej wywołania. MoŜe to dać zysk na czasie wykonania programu, chociaŝ zwykle zwiększa jego objętość. UWAGA: Funkcja z atrybutem inline nazywa się teŝ funkcją rozwijalną albo funkcją otwartą Przy okazji (2) 2. Atrybut inline moŝe być podany jawnie a treść metody na zewnątrz definicji klasy, ale wtedy naleŝy ją podać w pliku header'owym (.h) klasy, a nie w pliku implementacyjnym. Wynika to z faktu, Ŝe treść takiej metody potrzebna jest kompilatorowi w kaŝdym miejscu jej wywołania. 3. UŜycie odrębnego (niezaleŝnie kompilowanego) pliku zawierającego definicje metod ( posiadających atrybut extern ) pozwala ukryć szczegóły implementacyjne. UŜytkownik naszej klasy będzie korzystał tylko z pliku nagłówkowego w postaci źródłowej (.h) i skompilowanego pliku zawierającego kod metod (.obj). A więc np. wcale nie musi wiedzieć, jaki algorytm zastosowaliśmy do realizacji konkretnych obliczeń numerycznych. -13- -14- -15- -16-
Przy okazji (3) // charstack.h class CharStack void Push ( char e ) tab [ top++ ] = e; inline char Pop ( ); char Top ( ) const; inline char CharStack :: Pop ( ) return tab [ --top ];..... end of charstack.h // charstack.cpp char CharStack :: Top ( ) const return tab [ top - 1 ]; W powyŝszym przykładzie metody: Push i Pop mają atybut inline ( Push domyślnie, Pop jawnie ) metoda Top ma atrybut extern. Klasy wzorcowe - wprowadzenie (2) Poszukajmy 'kandydatów' na parametry. Zaznaczyłem je na kolorowo. Oczywiście nie wszystkie moŝliwości musimy wykorzystać. #define CSTACKSIZE 100 class CharStack char tab [ CSTACKSIZE ]; int size, top; CharStack ( ) size = CSTACKSIZE; top = 0; void Push ( char e ) tab [ top++ ] = e; char Pop ( ) return tab [ --top ]; char Top ( ) return tab [ top - 1 ]; int Size ( ) const return size; int Used ( ) const return top; int Place ( ) const return size top; void Display ( ) const cout << endl; for ( int i = 0; i < top; ++i ) cout << tab [ i ] << " "; A tak moŝe wyglądać nasz szablon: #define STACKSIZE 100 Klasy wzorcowe (1) T tab [ S ]; int size, top; Stack ( ) size = S; top = 0; void Push ( T e ) tab [ top++ ] = e; T Pop ( ) return tab [ --top ]; T Top ( ) return tab [ top - 1 ]; int Size ( ) const return size; int Used ( ) const return top; int Place ( ) const return size - top; void Display ( ) const cout << endl; for ( int i = 0; i < top; ++i ) cout << tab [ i ] << " "; #undef STACKSIZE I funkcja main: Klasy wzorcowe (2) int main ( ) const int k = 10; Stack<> S0; Stack<int> S1, S2; Stack<int,10> S3; Stack<double> S4; Stack<double,55> S5; Stack<double,k+5> S6; Stack<double,k+45> S7; cout << endl << S0.Size() << endl << S1.Size() << endl << S3.Size(); cout << endl << S4.Size() << endl << S5.Size() << endl << S6.Size(); S1.Push(10); S1.Push(11); S1.Push(12); S1.Push(13); S1.Push(14); S1.Display(); S2 = S1; S1.Pop(); S1.Pop(); S1.Display(); S2.Display(); //S3 = S1; // BŁĄD! //S4 = S1; // BŁĄD! ale S7 = S5; O.K. while ( S1.Place() && S2.Used() ) S1.Push( S2.Pop() ); S1.Display(); -17- -18- -19- -20-
Klasy wzorcowe (3) Program wyświetli (po zakomentowaniu wierszy z błędami): 100 100 10 100 55 15 10 11 12 13 14 10 11 12 10 11 12 13 14 10 11 12 14 13 12 11 10 Klasy wzorcowe (4) CharStack St; // obiekt St jest typu CharStack Stack<> S0; // obiekt S0 jest typu Stack<char, 100> Stack<int> S1, S2; // obiekty S1 i S2 są typu Stack<int, 100> Stack<int,10> S3; // obiekt S3 jest typu Stack<int, 10> Stack<double> S4; // obiekt S4 jest typu Stack<double, 100> Stack<double,k+5> S6; // obiekt S6 jest typu Stack<double, 15> Stack<CMPLX> SC; // obiekt SC jest typu Stack<CMPLX, 100> Kusząca (niebezpieczna) alternatywa: Klasy wzorcowe (1a) T tab [ S ]; T *p; int size; Stack ( ) p = tab; size = S; void Push ( T e ) *p++ = e; T Pop ( ) return *--p; T Top ( ) return *(p 1); int Size ( ) const return size; int Used ( ) const return p tab; int Place ( ) const return size (p tab); void Display ( ) const T *r = tab; while ( r < p ) cout << *r++ << " "; Na czym polega niebezpieczeństwo i jak mu zaradzić? Klasy wzorcowe (5) 1. Wzorzec klasy moŝe mieć więcej parametrów (ale nie mniej niŝ jeden!). 2. Opis parametru wzorca składa się ze słowa kluczowego class (ew. typename) lub nazwy typu wbudowanego oraz wybranego identyfikatora (nazwy parametru). Dla parametrów moŝna określać wartości domyślne. 3. Na liście parametrów wzorca kaŝdy parametr moŝe wystąpić tylko raz. 4. Parametr wzorca poprzedzony słowem kluczowym class (ew. typename) staje się specyfikatorem typu, którego moŝna uŝywać w pozostałej części definicji klasy wzorcowej (n.p. w deklaracjach pól, specyfikacjach parametrów metod et c.). 5. Parametr wzorca poprzedzony nazwą typu wbudowanego staje się stałą, której moŝna uŝywać np. do zapisu rozmiarów tablic, inicjowania wartości zmiennych et c. -21- -22- -23- -24-
Klasy wzorcowe (6) Zobaczmy inny wariant zapisu szablonu. // stack.h #include <iostream> using namespace std; #define STACKSIZE 1000 void Display ( ) const; Klasy wzorcowe (7) W dalszym ciągu pliku stack.h podajemy szablony metod: Stack< T, S > :: Stack ( ) p = q = tab; size = S; Stack< T, S >& Stack< T, S > :: operator= ( const Stack& rhs ) p = q; for ( T* r = rhs.q; r < rhs.p; *p++ = *r++ ); return *this; void Stack< T, S > :: Push ( T e ) *p++ = e; T Stack< T,S > :: Pop ( ) return *--p; T Stack< T, S > :: Top ( ) const return *(p - 1); int Stack< T, S > :: Size ( ) const return size; i dalej w pliku stack.h: Klasy wzorcowe (8) int Stack< T, S > :: Used ( ) const return p - q; int Stack< T, S > :: Place ( ) const return size - (p - q); void Stack< T, S > :: Display ( ) const T* r = q; while ( r < p ) cout << *r++ << " "; funkcje zaprzyjaźnione we wzorcach klas #undef STACKSIZE UWAGA! UWAGA! To wszystko powinno być podane w pliku stack.h. Dla klas wzorcowych nie tworzymy odrębnych plików implementacyjnych *.cpp. -25- -26- -27- -28-
Klasy wzorcowe (6) W rozpatrywanym wcześniej wzorcu klasy void Display ( ) const; chcemy zastąpić metodę Display zaprzyjaźnionym operatorem <<. Wzorce i friend (1) Próba zrealizowania tego zadania w następujący sposób: friend ostream& operator << (ostream&, const Stack<T,S>&); template < class T, int S> ostream& operator << (ostream& out, const Stack<T,S>& st) T* r = st.q; while ( r < st.p ) cout << *r++ << " "; return out; nie da oczekiwanego efektu. Operator << nie zostanie wygenerowany. Poprawny efekt otrzymamy pisząc: Wzorce i friend (2) template < class Q, int R> friend ostream& operator << (ostream&, const Stack<Q,R>&); template < class T, int S> ostream& operator << (ostream& out, const Stack<T,S>& st) T* r = st.q; while ( r < st.p ) out << *r++ << " "; return out; a nawet: Wzorce i friend (3) template < class Q, int R> friend ostream& operator << (ostream&, const Stack&); template < class T, int S> ostream& operator << (ostream& out, const Stack<T,S>& st) T* r = st.q; while ( r < st.p ) out << *r++ << " "; return out; -29- -30- -31- -32-
Jeszcze innym rozwiązaniem jest: Wzorce i friend (4) friend ostream& operator << (ostream& out, const Stack& st) T* r = st.q; while ( r < st.p ) out << *r++ << " "; return out; Koniec wykładu 10. -33- -34-