2010.07_Wielometody Rozszerzenie funkcji wirtualnych_[Programowanie C C++].pdf

(1036 KB) Pobierz
441723129 UNPDF
PROGRAMOWANIE C++
Wielometody
Rozszerzenie funkcji wirtualnych
Mechanizm funkcji wirtualnych pomaga wybrać metodę,
biorąc pod uwagę rzeczywisty typu obiektu, dla którego
daną metodę się woła. Jeżeli istnieje potrzeba wyboru
funkcji w zależności od dwóch lub większej ilości typów, to
odpowiedni mechanizm musimy dostarczyć sami.
Dowiesz się:
• Co to są wielometody;
• Jak implementować ten mechanizm w C++.
Powinieneś wiedzieć:
• Jak pisać proste programy w C++;
• Co to jest dziedziczenie i funkcje wirtualne;
• Co to jest wzorzec wizytatora;
• Co to jest programowanie generyczne.
alne) pozwala współdzielić kod: posługuje-
my się interfejsem do klasy bazowej, nato-
miast specyficzne dla danego typu operacje tworzymy
jako nadpisane funkcje wirtualne. Takie podejście jest
bardzo wygodne, dlatego jest dostarczane przez języ-
ki wspierające obiektowe podejście do programowania
(np. przez C++). Funkcje wirtualne pozwalają na wy-
bór odpowiedniej wersji nadpisanej metody, uwzględ-
niając rzeczywisty typ obiektu, dla którego tę meto-
dę wołamy. Mechanizm ten ma ograniczenia – wybie-
ramy funkcję składową w zależności od jednego ty-
pu Jeżeli istnieje potrzeba wyboru funkcji w zależno-
ści od dwóch lub większej ilości typów, to odpowied-
ni mechanizm musimy dostarczyć sami, jeżeli progra-
mujemy w C++. Mechanizm ten nazywany jest wielo-
metodą (ang. multimethods, multiple dispatch ). Pew-
ne obiektowe języki programowania dostarczają wie-
lometod, na przykład Common Lisp ma wbudowane
wielometody, Python – mechanizm ten dostępny jako
rozszerzenie. Poniższy artykuł pokazuje implementa-
cje wielometod w C++.
Przykład, którym będziemy się posługiwali, aby po-
kazać zastosowania wielometod, dotyczy hierarchii fi-
gur, pokazanej na Rysunku 1. Na początek przyjrzyj-
my się możliwościom, które dają funkcje wirtualne. Dla
figury możemy dostarczyć metodę obliczającą jej pole.
Aby wybierać odpowiedni algorytm obliczania pola, sto-
sujemy mechanizm późnego wiązania, ponieważ funk-
cja ta jest zależna od jednego typu (typu figury, dla któ-
rej obliczamy pole). Implementacja jest typowa: dostar-
czamy funkcję wirtualną w klasie bazowej, nadpisujemy
ją w klasach konkretnych, dostarczając odpowiedniego
algorytmu obliczania pola. Za wywołanie odpowiedniej
metody jest odpowiedzialny mechanizm funkcji wirtual-
nych dostarczany przez język, patrz Listing 1.
Mechanizm ten nie wystarczy do implementacji funk-
cji intersect , która zwraca pole przecięcia dwóch figur.
Jeżeli dysponujemy odpowiednimi funkcjami zawierają-
cymi algorytmy obliczania pola przecięcia kół, prostoką-
tów, prostokąta i koła itp., to problem sprowadza się do
odpowiedniego wyboru tych funkcji, w zależności od rze-
czywistych typów dwóch obiektów. Przykładowo, jeżeli
dysponujemy funkcją double intersect(const Circle&
a, const Circle& b) , która dostarcza pola przecięcia
dwóch kół, funkcją double intersect(const Circle& a,
const Rect& b) oraz funkcją double intersect(const
Rect& a, const Rect& b) , to należy wywołać odpowied-
nią, mając dwa uchwyty do typu Figure& (patrz Rysu-
nek 2). Wielometody umożliwiają takie wołanie, podob-
Szybki start
Aby uruchomić przedstawione przykłady, należy mieć do-
stęp do kompilatora C++ oraz edytora tekstu. Na wydru-
kach pominięto dołączanie odpowiednich nagłówków oraz
udostępnianie przestrzeni nazw, pełne źródła dołączono ja-
ko materiały pomocnicze.
20
7/2010
M echanizm późnego wiązania (funkcje wirtu-
441723129.036.png 441723129.037.png
 
441723129.038.png 441723129.001.png 441723129.002.png 441723129.003.png 441723129.004.png
Wielometody
nie jak funkcje wirtualne, gdy wybór dotyczy jednego ty-
pu.Wielometody umożliwiają takie wołanie.
cji double intersect(const Figure&, const Figure&) .
Funkcja ta otrzymuje dwa argumenty, ich typy nie są
znane, mamy uchwyty do klasy bazowej. Funkcja
intersect będzie wielometodą, będzie wołać odpo-
wiednią wersję przeciążonej funkcji, pokazanej na Li-
stingu 2, określając konkretne typy argumentów.
Wielometody wykorzystujące
rzutowanie dynamiczne
Celem naszego działania będzie dostarczenie funk-
Listing 1. Klasy reprezentujące �gury. Późne wiązanie pozwala tworzyć metody zależne od jednego typu (typu �gury)
class Figure { //klasa bazowa
public :
virtual double getArea () const = 0 ; //funkcja wirtualna, musi być nadpisana w klasie konkretnej
virtual ~ Figure () {}
} ;
class Rect : public Figure { //klasa konkretna
public :
virtual double getArea () const { //nadpisuje metodę
//oblicza pole prostokąta
}
//pozostałe metody i składowe
} ;
class Circle : public Figure { //inna klasa konkretna
public :
virtual double getArea () const { //nadpisuje metodę
//oblicza pole koła
}
} ;
Figure * f = new Rect (); //posługujemy się wskaźnikiem do klasy bazowej
double area = f -> getArea (); // wo ł a metod ę uwzgl ę dniaj ą c rzeczywisty typ obiektu ( tutaj typem tym jest Rect )
Listing 2. Funkcje obliczające odpowiednie pola przecięcia. Argumentami są typy konkretne
double intersect ( const Circle & a , const Circle & b ) ;//oblicza pole przecięcia dwóch kół
double intersect ( const Circle & a , const Rect & b ); //oblicza pole przecięcia prostokąta i koła
double intersect ( const Rect & a , const Rect & b ); // oblicza pole przeci ę cia dw ó ch prostok ą t ó w
Listing 3. Wielometody wykorzystujące rzutowanie dynamiczne. Implementacja bardzo niewydajna i trudna w pielęgnacji
double intersect ( const Figure & a , const Figure & b ) {//dostarczamy typów bazowych
if ( Circle * pa = dynamic_cast < Circle *>(& a )) {//bada typ pierwszego argumentu
if ( Circle * pb = dynamic_cast < Circle *>(& b )) {
return intersect (* pa , * pb ) ;//ma konkretne typy, woła dla dwóch kół
} else if ( Rect * pb = dynamic_cast < Rect *>(& b ) {
return intersect (* pa , * pb ) ;//woła przecięcie dla koła i prostokąta
}
} else if ( Rect * pa = dynamic_cast < Rect *>(& a )) {//bada typ pierwszego argumentu
if ( Circle * pb = dynamic_cast < Circle *>(& b )) {
return intersect (* pb , * pa ) ;//zamienia kolejność argumentów, aby wołać odp. funkcję
} else if ( Rect * pb = dynamic_cast < Rect *>(& b ) {
return intersect (* pa , * pb ) ;//woła przecięcie dla dwóch prostokątów
}
}
return 0.0 ;//nieznane typy
}
www.sdjournal.org
21
441723129.005.png 441723129.006.png 441723129.007.png 441723129.008.png 441723129.009.png 441723129.010.png 441723129.011.png 441723129.012.png
 
PROGRAMOWANIE C++
Wielometodę możemy realizować bezpośrednio, wy-
korzystując rzutowanie dynamiczne. Przykład imple-
mentacji pokazano na Listingu 3. Jest ona bardzo niewy-
dajna i trudna w pielęgnacji, ponieważ posługuje się łań-
cuchem operacji rzutowania typu dynamic_cast . W ciągu
instrukcji warunkowych badamy rzeczywisty typ pierw-
szego argumentu, a następnie rzeczywisty typ drugiego
argumentu. Wykonujemy przy okazji wiele operacji rzu-
towania dynamicznego, a każda z nich jest uznawana
za kosztowną, ponieważ przegląda listę klas w hierarchii
(a jeżeli stosujemy dziedziczenie wielobazowe, to prze-
gląda drzewo). Kod jest rozwlekły i zawiera powtórze-
nia (taki sam kod dla badania typu drugiego argumen-
tu, gdy typ pierwszego jest ustalony). Trudność w pielę-
gnacji wynika także z konieczności modyfikacji łańcucha
instrukcji warunkowych po zmianie hierarchii klas, gdyż
w łańcuchu tym badamy wszystkie typy. Dla dużej liczby
typów konkretnych kod znacznie się rozrasta, ponieważ
musimy uwzględnić wszystkie pary typów. Takie rozwią-
zanie ma jedną zaletę – jest bardzo proste. Opisane po-
niżej sposoby tworzenia wielometod będą starały się
usunąć wady przedstawionego rozwiązania.
Rysunek 1. Klasy reprezentujące �gury, wykorzystane do
demonstracji działania wielometod
Wykorzystanie wizytatora
Wybór funkcji w zależności od wielu typów można
oprzeć o wzorzec wizytatora. Wzorzec ten oraz jego
realizacja w C++ był tematem artykułu w SDJ 4/2010.
Rysunek 2. Funkcje obliczające pole przecięcia �gur gdy znany jest
ich konkretny typ
Listing 4. Wizytator dla hierarchii �gur
class Visitor {//wizytator bazowy
public :
virtual void visit ( const Rect &) = 0 ;
virtual void visit ( const Circle &) = 0 ;
} ;
class Figure {//klasa bazowa dostarcza metody accept
public :
virtual void accept ( Visitor & v ) const = 0 ;
//pozostałe metody
}
class Rect : public Figure {//klasa konkretna
public :
virtual void accept ( Visitor & v ) const {
return v . visit (* this ) ;//woła metodę visit(const Rect&) dla wizytatora
}
//pozostałe metody i składowe
} ;
class Circle : public Figure {//klasa konkretna
public :
virtual void accept ( Visitor & v ) const {
return v . visit (* this ) ;//woła metodę visit(const Circle&) dla wizytatora
}
//pozostałe metody i składowe
} ;
22
7/2010
441723129.013.png 441723129.014.png
 
441723129.015.png 441723129.016.png 441723129.017.png 441723129.018.png 441723129.019.png 441723129.020.png 441723129.021.png 441723129.022.png 441723129.023.png 441723129.024.png
 
Wielometody
Wizytator pozwala na wybór typu bez rzutowania dy-
namicznego. Klasy, dla których chcemy stosować to
rozwiązanie muszą zostać zmodyfikowane. Powinny
one dostarczać metodę accept . Należy także utworzyć
klasę wizytatora bazowego, patrz Listing 4.
Tworząc wielometodę, będziemy stosowali wizytator
wielokrotnie. Jeżeli rozważamy wybór funkcji w zależ-
ności od dwóch typów (najprostsza wersja wielometo-
dy), to wizytator stosujemy dwa razy, do wyboru pierw-
szego i drugiego argumentu. Wizytator nie wykorzy-
stuje rzutowania dynamicznego, dlatego sposób ten
jest bardzo wydajny. Wadą rozwiązania jest tworze-
nie klas pomocniczych (wizytatorów) oraz niebanal-
ne przepływy sterowania. Przykład dla przecięcia figur
pokazano na Listingu 5.
Wielometoda używa klasy pomocniczej,wizytato-
ra IntersectVisitor, który pozwala rozstrzygnąć, ja-
ki jest typ jednego (pierwszego) obiektu. Na wydru-
ku pokazano, że wizytator jest przekazany jako ar-
gument metody accept dla obiektu a, więc w odpo-
wiedniej metodzie visit dostaniemy obiekt a przeka-
zany jako typ konkretny. Mamy więc rzeczywisty typ
obiektu a .
Aby wyznaczyć rzeczywisty typ obiektu b , stosujemy
wizytator po raz drugi. W zależności od typu pierwsze-
go obiektu wizytatorem tym będzie CircleVisitor , je-
żeli obiekt a jest kołem, albo RectVisitor , jeżeli obiekt
a jest prostokątem. Różne klasy wizytatorów pomocni-
czych użytych do wyznaczania typu drugiego obiektu
są potrzebne, ponieważ przechowują one rzeczywisty
typ pierwszego argumentu.
Metody visit dla wizytatora wołanego na rzecz
obiektu b dostarczają typu tego obiektu. Wewnątrz
nich możemy wołać odpowiednią funkcję intersect ,
Listing 5. Wykorzystanie wizytatora do wyboru funkcji w zależności od dwu typów
struct CircleVisitor : public Visitor { //wizytator, gdy pierwszym typem jest koło
CircleVisitor ( const Circle & c ) : c_ ( c ) , value_ ( 0.0 ) {} //przekazuje jeden argument
virtual void visit ( const Circle & c ) { value_ = intersect ( c_ , c ); }//przecięcie koła z kołem
virtual void visit ( const Rect & r ) { value_ = intersect ( c_ , r ); }//przecięcie koła z prostokątem
const Circle & c_ ; //pierwszy obiekt przechowywany jako typ konkretny
double value_ ;//wynik
} ;
struct RectVisitor : public Visitor {//wizytator, gdy pierwszym typem jest prostokąt
RectangleVisitor ( const Rect & r ) : r_ ( r ) , value_ ( 0.0 ) {}
virtual void visit ( Rect & r ) { value_ = intersect ( r , r_ ); } //przecięcie prostokąta z prostokątem
virtual void visit ( Circle & c ) { value_ = intersect ( c , r_ ); } //przecięcie koła z prostokątem
Rect & r_ ; //pierwszy obiekt przechowywany jako typ konkretny
double value_ ;//wynik
} ;
struct IntersectVisitor : public Visitor { //rozstrzyga dwa typy, wykorzystuje wizytatory pomocnicze
IntersectVisitor ( const Figure & ig ) : ig_ ( ig ) , value_ ( 0.0 ) { }//przechowuje drugi obiekt
virtual void visit ( const Circle & c ) {//pierwszym typem jest koło
CircleVisitor circVisitor ( c ) ;//tworzy odpowiedni wizytator pomocniczych
ig_ . accept ( circVisitor ); //wybiera metodę w zależności od typu drugiego argumentu
value_ = circVisitor . value_ ; //przekazuje wynik obliczeń
}
virtual void visit ( const Rect & r ) { //pierwszy typ to prostokąt
RectVisitor rectVisitor ( r ); //tworzy odpowiedni wizytator
ig_ . accept ( rectVisitor );
value_ = rectVisitor . value_ ;
}
const Figure & ig_ ;//przechowuje jeden z obiektów
double value_ ; warto ść zwracana
} ;
double intersect ( const Figure & a , const Figure & b ) { / multimetoda wykorzystuj ą ca wizytator
IntersectVisitor visitor ( b );
a . accept ( visitor );
return visitor . value_ ;
}
www.sdjournal.org
23
441723129.025.png 441723129.026.png 441723129.027.png 441723129.028.png 441723129.029.png 441723129.030.png 441723129.031.png 441723129.032.png
 
PROGRAMOWANIE C++
ponieważ mamy konkretne typy obu argumentów. Wy-
niki obliczeń są przechowywane w składowej value_
wizytatora, a następnie zwracane użytkownikowi.
Przedstawione rozwiązanie jest wydajne, ale wymaga
ono żmudnego tworzenia klas pomocniczych (wizytato-
rów). Dla wielometody pozwalającej na wybór w zależ-
ności od dwóch typów, gdy różnych typów jest N, nale-
ży stworzyć N+1 wizytatorów. Tworzenie tych klas moż-
na automatyzować, wykorzystując metaprogramowanie
i bibliotekę boost::mpl ( patrz SDJ 12/2009 ). Przykład
szablonów dla wielometod zawiera biblioteka faif (patrz
ramka), ich opis zostanie zawarty w książce Średnioza-
awansowane programowanie w C++ .
Bezpośrednia implementacja
późnego wiązania
Późne wiązanie możemy zaimplementować bezpo-
średnio, wykorzystując wielowymiarową tablicę wskaź-
ników. Prosty przykład pokazano na Listingu 6.
Implementacja bezpośrednia wymaga dostarczenia
szeregu funkcji o tym samym interfejsie, więc musimy
posłużyć się operatorami rzutowania dynamicznego.
Więcej w książce
Zagadnienia dotyczące współcześnie stosowanych technik w języku C++, wzorce projektowe, programowanie generyczne, pra-
widłowe zarządzanie zasobami przy stosowaniu wyjątków, programowanie wielowątkowe, ilustrowane przykładami stosowany-
mi w bibliotece standardowej i bibliotekach boost, zostały opisane w książce ,,Średniozaawansowane programowanie w C++'',
która ukaże się niebawem.
Listing 6. Wykorzystanie tablicy wskaźników do funkcji do implementacji wielometod
//funkcja pomocnicza, dostarcza odpowiedni interfejs
double intersectCircleCircle ( const Figure & a , const Figure & b ) {
return intersect ( dynamic_cast < const Circle &>( a ) , dynamic_cast < const Circle &>( b ) );
}
double intersectCircleRect ( const Figure & a , const Figure & b ) {
return intersect ( dynamic_cast < const Circle &>( a ) , dynamic_cast < const Rect &>( b ) );
}
double intersectRectCircle ( const Figure & a , const Figure & b ) {
return intersect ( dynamic_cast < const Circle &>( b ) , dynamic_cast < const Rect &>( a ) );
}
double intersectRectRect ( const Figure & a , const Figure & b ) {
return intersect ( dynamic_cast < const Rect &>( a ) , dynamic_cast < const Rect &>( b ) );
}
double intersect ( const Figure & a , const Figure & b ) {
enum { CIRCLE_INDEX , RECT_INDEX , N } ;//indeksy dla typów oraz rozmiar tablicy
typedef double (* PF )( const Figure & , const Figure &) ;//typ wskaźnika na funkcję
static const PF CALL_TAB [ N ][ N ] = {//dwuwymiarowa tablica wskaźników na funkcje
{ & calculateCircleCircle , & calculateCircleRect } ,
{ & calculateRectCircle , & calculateRectRect }
} ;
struct IndexVisitor : public Visitor {//pomocniczy wizytator – zwraca indeks dla typu
IndexVisitor () : idx_ ( 0 ) {}
virtual void visit ( const Circle & ) { idx_ = CIRCLE_INDEX ; }
virtual void visit ( const Rectangle & ) { idx_ = RECT_INDEX ; }
int idx_ ;
}
visitorA , visitorB ;
a . accept ( visitorA );
b . accept ( visitorB );
PF fun = CALL_TAB [ visitorA . idx_ ][ visitorB . idx_ ] ;//pobiera wskaźnik z tablicy
return (* fun )( a , b ) ;//woła odpowiednią funkcję
}
24
7/2010
441723129.033.png 441723129.034.png 441723129.035.png
Zgłoś jeśli naruszono regulamin