23 kwietnia 2009

Polimorfizm statyczny

Główna metodą, dzięki której możemy zastosować polimorfizm to dziedziczenie, wraz z funkcjami (czysto) wirtualnymi. Jest to najbardziej intuicyjny sposób znany większości programistów. O tym, jak wygląda to od strony kompilatora, napisał swego czasu Xion w jednej ze swoich notek. Dziś jednak chciałbym się skupić na innej, rzadziej stosowanej, implementacji owej techniki.

Główną wadą polimorfizmu jest jego wydajność. Dziś, w czasach rozkwitu języków zarządzanych, nie jest to problem wyjątkowo dotkliwy, jednak w pewnych zastosowaniach, nawet tak niewielkie narzuty muszą być brane pod uwagę. Mowa tu oczywiście o dynamicznym wiązaniu metod, których klasa zostaje ustalona dopiero podczas wykonywania programu. Jak można obejść ten problem? Stosując szablony.

Przeanalizujmy prosty przykład: rysowanie i zarządzanie obiektami geometrycznymi.
Załóżmy, że chcemy przygotować klasę, która rysowałaby różnego rodzaju obiekty geometryczne: od punktów po wielokąty zdefiniowane przez użytkownika. Logicznym rozwiązaniem problemu, jest zastosowanie polimorfizmu.

Na początek, stwórzmy więc interfejs definiujący operacje jakie ma wykonywać owy obiekt:

class IGeometryObject{

    public:
        virtual void draw(void) = 0; //rysowanie obiektu na plotnie
        virtual void setColor(unsigned uColorType); //ustawia kolor obiektu

        virtual void create(void) = 0; //przydzielenie pamieci dla obiektu
        virtual void release(void) = 0; //zwolnienie zasobow obiektu
};

Jak widać, nie ma tu nic szczególnego – klika metod czysto wirtualnych. Stwórzmy teraz metody innej klasy, których zadaniem jest rysowanie takich obiektów:

class CGeometryDrawer{

    public:

        void drawObjectsList(const std::vector<IGeometryObject*> &vElems){
            for(unsigned i=0;i<vElems.size();i++)vElems[i]->draw();
        }

        void drawObject(IGeometryObject *pObject){pObject->draw();}
};

Główną zaletą takiego podejścia jest to, że kolekcja vElems może być heterogeniczna, czyli zawierać elementy różnych klas. Wystarczy tylko, że każda z nich będzie implementować interfejs IGeometryObject.

Był to więc przykład standardowego podejścia do problemu. Jak już wcześniej wspominałem, dla każdego wywołania jednej z wirtualnych metod interfejsu IGeometryObject musimy się liczyć z niewielkim narzutem, związanym z dynamicznym powiązaniem metody.

Czas na prezentację tytułowego sposobu. Podstawową różnicą będzie tu brak klasy, po której będzie obowiązek dziedziczenia – dzięki szablonom narzucimy odpowiedni interfejs wewnątrz metod:

class CGeometryDrawer{

    public:

        template <typename GeometryObject>
        void drawObjectsList(const std::vector<GeometryObject> &vElems){
            for(unsigned i=0;i<vElems.size();i++)vElems[i].draw();
        }

        template <typename GeometryObject>
        void drawObject(GeometryObject &object){object.draw();}
};

Jak widać, nowa wersja klasy rysującej korzysta z metod szablonowych. Metody te, definiując typ GeometryObject, narzucają dla niego obecność funkcji składowej draw. Dzięki temu, możemy przekazać obiekty dowolnych klas – istotne jest tylko to, by miały definicje draw. Ważne, by to metody były szablonowe, nie zaś cała klasa. W przeciwnym wypadku bylibyśmy ograniczeni tylko do obsługi typu przekazanego podczas definiowania obiektu klasy CGeometryDrawer.
Warto także zaznaczyć, że kolekcja vElems musi zawierać obiekty tego samego typu (homogeniczne). W przypadku polimorfizmu dynamicznego, dzięki zastosowaniu dziedziczenia, mogliśmy dodać do kolekcji, klasy różnych typów, w tym przypadku nie jest to jednak możliwe. Można co prawda, przekazać do metody drawObjectsList wektor wskaźników, ale wtedy funkcje składowe byłby wiązane dynamicznie, więc tracimy zyski związane ze statycznym wiązaniem.

Ogólnie rzecz ujmując polimorfizm dynamiczny można określić jako: powiązany i dynamiczny.

  • powiązany, ponieważ typy obiektów są zależne od innego typu (dziedziczenie po klasie bazowej, w tym przypadku interfejsie)
  • dynamiczny, bo klasa wywoływanej metody jest ustalana podczas działania programu.

Do jego podstawowych zalet można zaliczyć:

  • możliwość działania na kolekcjach heterogenicznych obiektów
  • metody korzystające z polimorfizmu mogą być dostarczane w postaci skompilowanej (lib lub dll)

Polimorfizm statyczny jest zaś niepowiązany i statyczny:

  • niepowiązany, bo klasy konkretne (niewirtualne) nie muszą być dziedziczone po określonym interfejsie lub innej klasie
  • statyczny – klasy metod są ustalane już na etapie kompilacji

Zalety:

  • większa wydajność
  • klasy konkretne nie muszą implementować całości określonego interfejsu, ponieważ wymagana jest znajomość tylko tych operacji które są rzeczywiście wywoływane w programie
  • istnieje możliwość stosowania typów podstawowych

2 komentarze:

  1. Fajna notka :) Można dodać, że tutaj zamiast dziedziczyć po podanej klasie i implementować pewien interfejs, typ którym parametryzujesz szablon musi po prostu spełniać pewne założenia, jak to żeby posiadać metodę draw czy określone operatory. Jak dany typ tego nie spełnia, to kompilator generuje długie i dziwne błędy :)

    OdpowiedzUsuń
  2. "(...)typ którym parametryzujesz szablon musi po prostu spełniać pewne założenia, jak to żeby posiadać metodę draw czy określone operatory"

    Było to ujęte w notce:

    "Metody te, definiując typ GeometryObject, narzucają dla niego obecność funkcji składowej draw. Dzięki temu, możemy przekazać obiekty dowolnych klas – istotne jest tylko to, by miały definicje draw."

    Choć o dziwnych błędach generowanych przez kompilator nie wspomniałem ;D

    OdpowiedzUsuń