Posts tagged ‘virtual methods’

override w C++, i nie tylko

2009-10-13 19:13

O braku słowa kluczowego override w C++ zdarzyło mi się już wspominać. Wskazywałem też, że jego niedobór jest całkiem istotnym brakiem, gdyż przypadki nadpisywania metod wirtualnych nie zawsze są zupełnie oczywiste.
Tym bardziej jest mi miło podzielić się odkryciem, które kiedyś poczyniłem na forum Warsztatu. Dowiedziałem się mianowicie, że słówko to do C++ można… dodać!

Jak to? – słychać pewnie pełne zdziwienia głosy. Ano bardzo prosto; zostało to zresztą wyczerpująco przedstawione w oryginalnym poście Esidara. Tutaj pokrótce tylko nakreślę jego ideę, która sprowadza się do genialnego spostrzeżenia, iż C++ zasadniczo radzi sobie bez override. A skoro tak, to jego wprowadzenie jest banalnie i wymaga zaledwie jednej linijki kodu:

  1. #define override

I już! Składniowo można go teraz używać dokładnie tak samo jak analogicznego słówka z C#… albo właściwie jakkolwiek inaczej – implementacja jest bowiem na tyle elastyczna, że zaakceptuje każde jego wystąpienie :) To aczkolwiek jest też, niestety, jego wadą…

W porządku, żarty na bok. Żeby ta notka nie była jednak zupełnie bezużyteczna, pokażę jeszcze, jak przekonać Visual C++, by nasze nowe słówko było przezeń kolorowane tak, jak inne słowa kluczowe w C++.
Da się to zrobić, tworząc (lub edytujący istniejący) plik UserType.dat, umieszczony w tym samym katalogu co plik wykonywalny Visual Studio (devenv) – czyli gdzieś w czeluściach Program Files, zapewne. W tymże pliku wpisujemy w nowej linijce override. Jeśli przypadkiem chcielibyśmy kolorować też inne, niestandardowe słowa kluczowe (to pewnie jest bardziej prawdopodobne), to każde z nich należy umieścić w osobnej linijce.
Po zapisaniu pliku i restarcie VS wystarczy teraz wejść w opcje programu, do zakładki Environment > Fonts & Colors, a tam na liście Display items zaznaczyć User Keywords i przyporządkować im wybraną czcionkę i/lub kolor. Możliwe zresztą, że w ogóle nie musimy tego robić, jako że domyślnie własne słowa kluczowe kolorowane są tak, jak te “oryginalne”.

Typ zwracany funkcji wirtualnej

2009-08-04 16:42

Kiedy w klasie pochodnej piszemy nową wersję funkcji wirtualnej, to w C++ powinniśmy zawsze uważać na to, żeby jej nagłówek był identyczny z tym, który zdeklarowano w klasie bazowej. To dlatego, że składnia języka nie wymaga w tej sytuacji żadnego słowa kluczowego w rodzaju override. Jeśli więc coś nieopatrznie zmienimy, to nie uzyskamy nowej wersji funkcji, tylko zupełnie nową funkcję przeciążoną:

  1. class Foo
  2. {
  3.     public: virtual void Do(int x, int y, int z) { }
  4. };
  5.  
  6. class Bar : public Foo
  7. {
  8.     // to *nie* jest nowa wersja Foo::Do, tylko zupełnie inna funkcja
  9.     public: void Do(int x, int y, string z) { }
  10. };

Taki efekt jest jest w gruncie rzeczy oczywisty i łatwy do zrozumienia. Pewnie więc będzie nieco zaskakujące dowiedzieć się, że tak naprawdę postać funkcji wirtualnej w klasie pochodnej może się trochę różnić, a mimo to wciąż będziemy mówili o tej samej funkcji.

Jak to jest możliwe?… Dopuszcza się mianowicie różnicę w typie zwracanym przez nową wersję funkcji wirtualnej. Może on być inny od typu zwracanego przez wersję z klasy bazowej – ale tylko pod warunkiem, że:

  • pierwotny typ był wskaźnikiem lub referencją do klasy (a nie chociażby typem podstawowym, jak int)
  • nowy typ zwracany jest – odpowiednio – wskaźnikiem lub referencją do klasy pochodnej od tej z typu pierwotnego
  • nowy typ posiada tę samą kwalifikację const/volatile co typ pierwotny

Przykładowo więc, jeśli w klasie bazowej funkcja wirtualna zwraca const A*, to w klasie pochodnej może zwracać const B*, o ile klasa B dziedziczy publicznie po A. Nie może za to zwracać samego B* (niestałego) lub const X*, gdy klasa X jest niezwiązana z A.

Do czego taki “myk” może się przydać? Najczęściej chodzi tutaj o sytuacje, gdy mamy do czynienia z równoległym dziedziczeniem kilku klas, które na każdym poziomie są związane ze sobą. Mogę sobie na przykład wyobrazić ogólny menedżer zasobów w grze, którego metoda wirtualna Get zwraca wskaźnik na bazową klasę Resource, a następnie bardziej wyspecjalizowany TextureManager, który w tej sytuacji podaje nam wskaźnik na Texture. (Oczywiście klasa Texture w założeniu dziedziczy tu po Resource). Czasami coś takiego może być potrzebne, aczkolwiek takie równoległe hierarchie dziedziczenia nie są specjalnie eleganckie ani odporne na błędy.
Lepszym przykładem jest wirtualny konstruktor kopiujący: metoda, która pozwala na wykonanie kopii obiektu bez dokładnej znajomości jego typu. Zwyczajowo nazywa się ją Clone:

  1. class Object
  2. {
  3.     public:
  4.         virtual Object* Clone() const { return new Object; }
  5. };
  6.  
  7. class Foo : public Object
  8. {
  9.     public:
  10.         // nowa wersja metody Object::Clone
  11.         Foo* Clone() const { return new Foo; }
  12. };

Dzięki temu że metoda jest wirtualna, można ją wywołać nie znając rzeczywistego typu obiektu (co nie jest możliwe w przypadku zwykłych konstruktorów, które w C++ wirtualne być nie mogą). W wyniku jej wywołania otrzymamy jednak tylko ogólny wskaźnik Object* na powstałą kopię obiektu.
Gdybyśmy teraz nie zmienili typu zwracanego przez metodę w klasie pochodnej, to klonowanie podobne do tego:

  1. void Function(Foo* foo)
  2. {
  3.     Foo* copy = foo->Clone();
  4.     // ...
  5. }

wymagałoby dodatkowego rzutowania w górę, by mogło zostać przepuszczone przez kompilator.

Tags: , , , , ,
Author: Xion, posted under Programming » Comments Off on Typ zwracany funkcji wirtualnej

Metody wirtualne i override

2008-08-29 15:43

W wielu językach obiektowych występuje słowo kluczowe override, którego dodawanie jest obowiązkowe podczas nadpisywania metod wirtualnych w klasach pochodnych (w przeciwnym wypadku generowanie jest ostrzeżenie). Jak można się spodziewać, C++ do takich języków nie należy :) A szkoda, bo wymóg stosowania override zapobiega pomyłkom przy nadpisywaniu (np. literówkom w nazwach metod), które można wykryć dopiero w trakcie działania programu, gdy ze zdziwieniem odkrywamy, że zamiast nowej wersji metody wywoływana jest stara.

Można częściowo temu zaradzić, ale sposób jest brzydki. Polega on na stworzeniu makra zawierającego całą definicję klasy (albo przynajmniej deklaracje jej metod wirtualnych):

  1. #define DECLARE_FOO(__suffix__) \
  2.     public: \
  3.         virtual void AbstractDoSomething() __suffix__; \
  4.         virtual void DoSomethingElse();

Następnie używamy go zarówno w definicji klasy bazowej, jak i pochodnych:

  1. class Foo
  2. {
  3.     DECLARE_FOO(=0)
  4. };
  5.  
  6. class Bar : public Foo
  7. {
  8.     DECLARE_FOO( )
  9. };

Dodatkowy parametr pozwala odróżnić obie sytuacje, co jest koniecznie w przypadku metod czysto wirtualnych (które nie posiadają implementacji).

Sztuczka jest być może pomysłowa, ale jednocześnie dość przygnębiająca. Dlaczego bowiem tak prosta sprawa jak zapobieganie pomyłkom przy nadpisywaniu metod wirtualnych musi być w C++ realizowana za pomocą brzydkich makr?…

Tags: , ,
Author: Xion, posted under Programming » 9 comments

Jak działają metody wirtualne

2008-07-24 17:16

Zauważyłem, że niektóre osoby mają problem z właściwym wykorzystaniem metod wirtualnych i polimorfizmu, nie widząc czasami możliwości ich wykorzystania nawet w najbardziej oczywistych przypadkach. Być może powodem jest coś w rodzaju braku zaufania do całego mechanizmu, który jest spowodowany wrażeniem, że kompilator dokonuje przy jego okazji jakichś pseudomagicznych sztuczek.

A tak naprawdę z metodami wirtualnymi sprawa jest dosyć prosta. Pomiędzy nimi a zwykłymi metodami różnica polega wyłącznie na sposobie ich wywoływania.
W tym pierwszym przypadku użycie zwykłej metody względem obiektu:

  1. CObject* pObject = new CObject;
  2. // ...
  3. pObject->Method();

jest zamieniane w wynikowym kodzie na normalne wywołanie funkcji z dodatkowym parametrem:

  1. CObject_Method (pObject); // nazwa tej funkcji jest czysto umowna

Ten parametr jest potem dostępny jako wskaźnik this wewnątrz metody i pozwala odwoływać się do innych składowych obiektu.

W przypadku metod wirtualnych wywołanie jest natomiast dwuetapowe i polega na wykorzystaniu pewnych dodatkowych informacji. Są nimi: tablica funkcji wirtualnych (zwana vtable) oraz wskaźnik na nią (czyli vptr). Tablica występuje tylko w jednej kopii na całą klasę i zawiera tyle elementów, ile funkcji wirtualnych klasa ta posiada. Jej elementami są po prostu adresy w pamięci tych właśnie funkcji: pierwsza funkcja wirtualna ma więc adres zapisany w elemencie vtable o indeksie 0, druga – o indeksie 1, itd.


Tablica metod wirtualnych i wskaźnik na nią (Źródło)

Vptr jest natomiast daną trzymaną wraz z każdym obiektem, podobnie jak zwykłe pola tego obiektu (często vptr jest umieszczany w pamięci tuż po nich). Jest on niczym innym, jak wskaźnikiem na vtable i służy obiektowi, gdy ten chce wywołać którąś ze swoich metod wirtualnych. Dokładniej wygląda to mniej więcej tak:

  1. // (wywołanie pObject->Method(), jeśli Method() jest wirtualna)
  2. pMethod = pObject->_vptr[0]; // załóżmy, że metoda ta ma indeks 0
  3. (*pMethod)(pObject);

Widać więc, że polega to po prostu na pobraniu odpowiedniego adresu metody z vtable, kierując się indeksem ustalanym w czasie kompilacji, zwykle na podstawie kolejności deklaracji metod wirtualnych w bloku class.

Dlaczego jednak raz może być wywołana wersja metody z klasy bazowej, a raz z pochodnej?… Tego nietrudno się już chyba domyślić: wszystko zależy od tego, na jaką tablicę pokazuje vptr. Jego ustawienie następuje automatycznie w trakcie tworzenia obiektu; wtedy wiadomo oczywiście, jakiego on jest typu. Potem jednak może on być dostępny zarówno przez wskaźnik do swojej klasy, jak i do klasy bazowej. W obu przypadkach metody wirtualne będą działały poprawnie, gdyż w ich wywołaniu będzie pośredniczył vptr.

Tak to wszystko wygląda, w wielkim skrócie rzecz jasna :) Całość nie jest może trywialna ze względu na pewne kombinacje ze wskaźnikami do funkcji, jednak nie zaszkodzi mieć przynajmniej ogólne pojęcie o tym, jak to właściwie działa. Istnieje szansa, że dzięki temu będziemy potrafili korzystać nieco lepiej z metod wirtualnych, a przy okazji trochę więcej rozumieć z tajemniczych dyskusji “ekspertów C++” przerzucających się takimi tajemniczymi terminami jak vtable i vptr ;-]

Tags: ,
Author: Xion, posted under Programming » 5 comments

Przesłanianie kontra nadpisywanie

2008-02-14 19:08

Jednym ze swego rodzaju kruczków programowania obiektowego jest różnica między przesłanianiem (hide) a nadpisywaniem (override) metod w klasach pochodnych. Niby nic w tym trudnego, dopóki nie uświadomimy sobie, że metody wirtualne – w przeciwieństwie do zwykłych – mogą podlegać obu tym procesom i że w ich przypadku różnią się one w dość istotny sposób.

Jeśli mianowicie mamy jakąś metodę wirtualną w klasie bazowej, to w klasie pochodnej teoretycznie możemy ją zarówno nadpisać, jak i przesłonić. Gdy zdecydujemy się na to pierwsze wyjście, wówczas nasz obiekt staje się polimorficzny. Posługując się wskaźnikiem lub referencją do obiektu klasy bazowej tak naprawdę możemy operować na obiekcie klasy pochodnej, a wywołując metodę wirtualną nie wiemy, która jej wersja będzie użyta. Wiemy jedynie, że będzie to ta “właściwa” (z klasy, do której należy nasz obiekt). No cóż, to podstawa OOP-u, jakkolwiekby na to nie patrzeć :)

  1. // klasa bazowa
  2. class CBase { public: virtual void Foo() { /* ... */ } };
  3.  
  4. // klasa pochodna
  5. class CDerived : public CFoo
  6. {
  7.    public:
  8.       // nadpisana metoda
  9.       void Foo()   { /* ... */ }
  10. };

Nie zawsze jednak musi tak być. Jeżeli metodę wirtualną się przesłoni, wtedy nic takiego nie zadziała i zostanie wywołana wersja z klasy bazowej. Typ obiektu nie zostanie bowiem rozpoznany w trakcie działania programu, a całe wywołanie będzie zapisane “na sztywno” w wynikowym kodzie. W gruncie rzeczy będzie tak samo, jakbyśmy przesłoni dowolną inną (niewirtualną) metodę.
O ile oczywiście możemy, bowiem w C++ metody wirtualnej przesłonić się po prostu nie da. Nadpisywanie jest po prostu wykonywane automatycznie i nie można z niego zrezygnować. W innych językach (jak w Delphi i C#) wymaga ono słowa kluczowego override. Tam też metody wirtualne można przesłaniać, stosując modyfikatory – odpowiednio reintroduce i new.

Żeby było zabawniej, przesłanianie i nadpisywanie może występować dla tej samej metody w tej samej hierarchii dziedziczenia. Metoda wirtualna zdefiniowana w klasie A może być na przykład nadpisywana w kolejnych klasach pochodnych A1, A2, A3, itd. I nagle w klasie B zostanie ona przesłonięta. Ba, ta przesłonięta wersja sama może być wirtualna i nadpisywana w dalszych klasach pochodnych: B1, B2, B3, …
Nie spotyka się tego często, jako że podobne wielopiętrowe drzewa dziedziczenia są rzadkie (i takie być powinny). Warto jednak o tym wiedzieć, bo nigdy nie wiadomo, kiedy podobny kruczek przybierze w naszym kodzie postać wielkiego kruka i zacznie nas dziobać zagadkowymi błędami ;]

Tags: , ,
Author: Xion, posted under Programming » 1 comment
 


© 2023 Karol Kuczmarski "Xion". Layout by Urszulka. Powered by WordPress with QuickLaTeX.com.