Statyczny polimorfizm
6 stycznia 2009
Głównym sposobem wspierania polimorfizmu w prawie wszystkich popularnych implementacjach C++, a także innych języków programowania, jest korzystanie z tablic funkcji wirtualnych vtables. Wiążą się z tym jednak pewne nieudogodnienia. Funkcje szablonowe nie mogą być jednocześnie funkcjami wirtualnymi, jest to co prawda całkiem logiczne, ale z drugiej strony czasami staje się problemem. Tego typu trudności można łatwo pokonać korzystając z poprzednio opisanych thin templates. Zupełnie inną klasą problemów jest narzut związany z wywoływaniem funkcji wirtualnych, a także sama obecność vtables, które przy rozbudowanych klasach mogą osiągnąć pokaźne rozmiary. Sposobem na pozbycie się spadku wydajności spowodowanego dynamiczną obsługą mechanizmu, także i w tej sytuacji jest statyczny sposób rozwiązania problemu. Warto zapoznać się ze statycznym polimorfizmem znanym także jako simulated dynamic binding.
Curiously recurring template pattern
Podstawą statycznego polimorfizmu jest wzorzec opisany przez Jamesa Copliena zwany curiously recurring template pattern (CRTP). Pozwala on na przekazanie za pomocą szablonów do klasy bazowej informacji o typie klasy pochodnej. Cały mechanizm najlepiej prezentuje poniższy przykład:
class derived : public base<derived> {};
Dzięki dodatkowej informacji jaką dysponuje klasa bazowa, ma ona dostęp do klasy pochodnej. Pozwala to na wygodne zaimplementowania wzorca singleton, licznika instancji klasy i wielu innych mechanizmów. Jak się okaże ten idiom jest także głównym elementem statycznego polimorfizmu. Można się także spotkać z podobnymi konstrukcjami typu:
template<typename base> class derived : public base {};
Pozwalają one między innymi na swobodne ustalanie hierarchii klas, a także na wprowadzenie mechanizmu wytycznych. Jak łatwo zauważyć wiele idei powiązanych z tak zwanym nowoczesnym C++ jest oparta właśnie na CRTP.
Simulated dynamic binding
Statyczny polimorfizm został spopularyzowany przez ATL oraz WTL. Polega on na wykorzystaniu przez klasę bazową informacji o typie klasy pochodnej do wywołania odpowiedniej metody. Przedstawia to poniższy przykład:
template<typename T> class base { public: int metoda1() { T *ptr = static_cast<T*>(this); return ptr->metoda2(); } }; class derived : public base<derived> { public: int metoda2(); };
Klasa bazowa jest w stanie wywołać funkcję odpowiedniej klasy pochodnej dzięki temu, że ma informację o niej. Po rzutowaniu this na odpowiedni typ T. Zostanie wywołana funkcja składowa właśnie tego typu. Oczywiście przedstawiony przykład nie posiada żadnego wsparcia dla sytuacji gdy programista źle użyje CRTP i jako T przekaże typ w tej sytuacji niepoprawny. Na wygodny sposób wygenerowania w takiej sytuacji czytelnego komunikatu o błędzie przez kompilator, trzeba niestety czekać do pojawienia się C++0x i concepts.
Statyczne składowe klasy
Jedną z najciekawszych rzeczy na jakie pozwala ten mechanizm jest polimorfizm statycznych funkcji składowych. Oto przykład analogiczny do poprzedniego, z tym że wykorzystane są statyczne funkcje składowe:
template<typename T> class base { public: static int metoda1() { return T::metoda2(); } }; class derived : public base<derived> { public: static int metoda2(); };
Dzięki temu, statyczne funkcje składowe klasy bazowej mogą bez przeszkód wywoływać statyczne funkcje składowe klasy pochodnej. Daje to większe pole manewru przy projektowaniu między innymi fabryk obiektów. Patrząc na to bardziej ogólnie, pozwala na korzystanie w statycznych funkcjach składowych z dobrodziejstwa programowania zorientowanego obiektowo jakim jest polimorfizm.
Konsekwencje stosowania statycznego polimorfizmu
W zaprezentowanym kodzie nie są wykorzystywane, dynamiczne elementy implementacji języka, a w szczególności funkcje wirtualne. Dzięki temu problem narzutu związanego z wywoływaniem wskaźników do funkcji jest rozwiązany, tak samo jak problem zwiększania się rozmiaru pliku binarnego przez obecność wielu vtables. Także rozmiar pojedynczej instancji klasy zmniejsza się o rozmiar wskaźnika na danej platformie, co może mieć duże znaczenie, w przypadku gdy gromadzone są pokaźne ilości obiektów. Oczywiście nie można tutaj pominąć także możliwości zastosowania tego mechanizmu dla funkcji statycznych tym bardziej, że jest to rzecz niemożliwa do zrealizowania jedynie za pomocą funkcji wirtualnych.
Nie sposób jednak nie powiedzieć o wadach tego rozwiązania. Gdy stosujemy funkcje wirtualne, kompilator wykonuje całą pracę związaną z implementacją polimorfizmu za programistę. Tutaj jest inaczej, więcej kodu należy napisać ręcznie, co może go czasami niepotrzebnie skomplikować. Nie można w bezpieczny sposób gromadzić instancji różnych klas pochodnych dziedziczących po tych samych klasach bazowych, w tej sytuacji klasy bazowe base, base są zupełnie różnymi klasami. Dlatego też jakiekolwiek rzutowania na klasę bazową mijają się z celem, ponieważ i tak nie ma możliwości na pozbycie się informacji o klasie pochodnej. Sprawia to, że główny zakres wykorzystania statycznego polimorfizmu to wywoływanie funkcji składowych klasy pochodnej z wnętrza klasy bazowej. To wyjaśnia dlaczego technika ta upowszechniła się głównie w API takich jak ATL i WTL.
Podsumowanie
Statyczny polimorfizm na który można natknąć się także pod nazwami simulated dynamic binding, ATL style inheritance lub upside down inheritance, jest niewątpliwie bardzo ciekawą techniką. Z drugiej strony należy pamiętać, że zdecydowanie nie nadaje się on do używania przy każdej możliwej okazji. Jest to mechanizm, który należy stosować wtedy kiedy potrafi przynieść duże korzyści a jego wady nie są istotne. W każdej innej sytuacji niepotrzebnie zaśmieca kod i utrudnia życie programistom.
Komentarze do wpisu "Statyczny polimorfizm":
1.
lionix napisał(a):
6 stycznia 2009, 22:00:35
Albo mi się wydaje albo pomyliłeś funkcje szablonowe z metodą wirtualną w szablonie.
2.
Paweł Dziepak napisał(a):
6 stycznia 2009, 22:16:14
Rozumiem, że chodzi o fragment:
"Funkcje szablonowe nie mogą być jednocześnie funkcjami wirtualnymi"
Funkcja wirtualna musi być (niestatyczną) funkcją składową, więc wiadomo, że chodzi tutaj o metodę. Natomiast funkcja składowa może być wirtualna w szablonie klasy, ale sama nie może być szablonem. Wynika to z braku możliwości określenia rozmiaru tablicy metod wirtualnych przez kompilator w tym drugim wypadku.
W standardzie C++ nie ma żadnej wzmianki o tym że funkcja składowa klasy szablonowej nie może być wirtualna (14.5.1.1), lecz jest informacja że szablonowa funkcja składowa nie może być wirtualna (14.5.2/3).
3.
bigfun napisał(a):
6 stycznia 2009, 23:00:21
Co to jest Derive1 ?
4.
lionix napisał(a):
6 stycznia 2009, 23:00:35
Sory, źle pomyślałem że jak mówisz o thin templates to że może chodzi ci o metody wirtualne w szablonie.
A co do C++0x to faktycznie, concepts to to czego najbardziej brakuje.
5.
Paweł Dziepak napisał(a):
7 stycznia 2009, 00:11:27
@bigfun: Wybacz przykład miał pierwotnie wyglądać trochę inaczej i widocznie umknęło mi to jak go modyfikowałem. Oczywiście chodziło o dervied. Poprawka już wprowadzona.
Dodaj komentarz: