C++ bez cholesterolu

Polimorfizm i metody wirtualne

Metody wirtualne

No wreszcie! Zapowiadałem go już kilka razy, więc przydałoby się wreszcie tym zająć. Wyobraźmy sobie, co się stanie, jeśli przedefiniujemy metodę w klasie pochodnej:

#include <iostream>

class A {
  public:
  int Szkapa()
  { cout << "Bazowa"; }
};

class B: public A {
  public:
  int Szkapa()
  { cout << "Pochodna"; }
};

Powiedzieliśmy sobie, że każdy obiekt należący do pewnej klasy, należy też do jej klasy pochodnej. Jednak w danym przypadku to stwierdzenie może zlekka zawodzić. Jak wiemy, obiekt klasy pochodnej może być trzymany za wskaźnik do klasy bazowej, stwórzmy więc taki obiekt:

int main() {
  B b;
  A* p = &b;           // w porządku
  int a = p->Szkapa(); // i co?
  return 0;
}

I choć `p' wskazuje na obiekt typu `B', to jednak zostanie wywołane A::Szkapa. Przecież ta metoda to zwykła funkcja. Skąd kompilator ma niby wiedzieć, że obiekt, na który wskazuje `p' jest obiektem typu `B'? Za chwile w tym samym kodzie ten sam wskaźnik może wskazać na obiekt innego przecież typu. Zatem tutaj informacja o dodatkach klasy B została utracona. Język C++ w zwykłych klasach nie umieszcza informacji o typie, która pozwalałaby go identyfikować. Nie oznacza to jednak, że w C++ mamy tylko takie klasy. Dodajmy zatem w deklaracji klasy A przed nazwą metody słowo kluczowe `virtual':

class A {
  public:
  virtual int Szkapa()
  { cout << "Bazowa"; }
};

Zmiana w klasie B nie jest już konieczna. Każda klasa, w której zadeklarowano choć jedną metodę wirtualną, staje się klasą POLIMORFICZNĄ i zawiera w sobie informacje, pozwalające na identyfikację typu. Natomiast każda metoda, która została zadeklarowana jako wirtualna, podlega DYNAMICZNEMU WIĄZANIU.

Na czym to polega? Proszę uruchomić program po wniesieniu naszej modyfikacji i okaże się, że została wywołana funkcja B::Szkapa. Polimorficzność klasy oczywiście podlega dziedziczeniu, tak samo jak wirtualność metod. Oznacza to, że jeśli metoda wirtualna zostanie przedefiniowana w klasie pochodnej, staje się również metodą wirtualną.

Konstrukcja i destrukcja w obiektowości

Teraz następna rzecz. Przypomnijmy sobie najpierw, do czego służą destruktory. Wyobraźmy sobie, że należy wybudować jakiś budynek i należy zorganizować budowę. Skupmy się na razie na tym, że na budowie musi pracować dźwig. Należy więc ustawić płyty betonowe, potem przypiąć szyny, następnie przetransportować części dźwigu i poskładać go do kupy. Kiedy budowa się zakończy, należy złożyć całą budowę i m.in. rozmontować dźwig. Należy zatem wykonać czynności odwrotne i w odwrotnej kolejności. Najpierw więc trzeba rozłożyć dźwig na części, odtransportować, następnie rozkręcić szyny, a potem zdjąć płyty betonowe. Widzieliśmy tutaj, że dźwig jest zależny od środków transportu oraz od szyn, a szyny z kolei od płyt betonowych.

Takie przykłady możnaby oczywiście mnożyć. Nie ma w tym zresztą nic dziwnego, przecież programowanie obiektowe to nie jest technologia przypadkowo wzięta z powietrza, ani nie wykiełkowała w głowach zwariowanych naukowców, tylko została - mówiąc szczerze - zerżnięta z obserwacji codziennego życia.

Tutaj nasza klasa B jest zależna od A, tzn. aby istniała cała B, musi istnieć jej podstawa, A. Podobnie, przy zwalnianiu obiektów, najpierw powinno się zniszczyć część "nadbudówki" klasy `B' w stosunku do `A', a następnie to, co należy do klasy A. Dodajmy zatem destruktory do obu klas i zobaczmy, jak się one wywołają.

#include <iostream>
using namespace std;

class A {
  public:
  virtual int Szkapa()
  { cout << "Bazowa"; }
  ~A() { cout << "Niszczę A\n"; }
};

class B: public A {
  public:
  int Szkapa()
  { cout << "Pochodna"; }
  ~B() { cout << "Niszczę B\n"; }
};

int main() {
  A* pa = new B;
  pa->Szkapa();
  delete pa;
  return 0;
};

I tutaj proszę zwrócić uwagę na ten szczegół. Tworzymy obiekt normalnie konstruktorem, ale do operatora delete obiekt idzie przez wskaźnik na klasę A. Jest to dość niebezpieczna konstrukcja. Jak się można spodziewać i tu wszystko działa źle; wywołany jest bowiem destruktor klasy A. Jednak destruktor - jak i każda inna metoda - może być wirtualny. Zadeklarujmy zatem konstruktor klasy A jako wirtualny i spróbujmy ponownie.

Jak widać wszystko teraz przebiegło prawidłowo; wywołały się oba destruktory w odpowiedniej kolejności.

Są tacy, którzy pewnie chcieliby zapytać, czy skoro istnieje wirtualny destruktor, to czy może istnieć też wirtualny konstruktor. Niektórzy nawet podchodzą do tego pytania poważnie, choć dla mnie takie pytanie jest równie poważne, jak pytanie, czy strażacy, jak w czasie alarmu zjeżdżają po rurze (jak niektórzy wiedzą, stosują "zjeżdżanie po rurze" w celu zminimalizowania czasu dostawania się do pomieszczenia z samochodem w czasie alarmu), to czy po powrocie z wyjazdu również "wjeżdżają" po tej rurze z powrotem.

No dobrze, ale gdyby zaryzykować nawet poważne podejście do tego tematu, to należy odpowiedzieć sobie na następujące pytania: Czym jest konstruktor? Skąd się bierze konstruktor do skonstruowania obiektu określonej klasy? Czym jest metoda wirtualna? Odpowiedzi są następujące: Konstruktor jest metodą odpowiadającą za inicjalizację obiektu. Do skonstruowania obiektu określonej klasy bierze się konstruktor z tej właśnie klasy. Metoda wirtualna służy do tego, żeby została wywołana jej wersja zdefiniowana w tej klasie, w której utworzono obiekt, niezależnie od tego, przez co dany obiekt jest aktualnie widziany.

Zwracam szczególną uwagę na słowo "utworzono". Wskazuje to jednoznacznie na fakt, że metoda wirtualna ma sens tylko wtedy, jeśli wywołano ją na rzecz obiektu, któremu już wywołano konstruktor. Konstruktor działa na nieutworzonym obiekcie i dopiero po zakończeniu jego działania obiekt uważa się za utworzony. Z tego właśnie względu określenia "metoda wirtualna" i "konstruktor" wzajemnie się wykluczają (zwracam też uwagę, że konsekwencją tego jest reguła, że w konstruktorze nie da się wywołać metody wirtualnej; jeśli interesują cię szczegóły, przejdź do rozdziału pt. "Smaczki").

Niektórym dodatkowo merda się określenie "konstruktor" i "konstrukcja obiektu". Otóż konstrukcja obiektu składa się z dwóch etapów: przydziału pamięci i wywołaniu konstruktora. Zatem konstruktor w C++ jest tylko częścią odpowiadającą za inicjalizację obiektu, a nie za jego "utworzenie". To jest zresztą również m.in. przyczyną tego, że implementację owego "wirtualnego konstruktora" niektórzy upatrują w jednym ze schematów projektowych zwanym "fabryką obiektów". Jest tam taki myk, że funkcja zwraca wskaźnik do klasy podstawowej, ale może zwracać obiekty efektywnie różnych typów. Przyczyną całej pomyłki jest właśnie błędne rozumienie słowa "konstruktor": wspomniana funkcja (czy metoda) odpowiada za utworzenie obiektu, a nie za jego konstrukcję. Zastępuje bowiem nie tylko konstruktor, ale również operator new.

Aspekty obiektowe typowe dla C++

Proszę zatem zapamiętać podstawowe reguły warunkujące poprawne programowanie obiektowe w C++:

  1. W klasie polimorficznej destruktor MUSI być wirtualny. Co prawda kompilator tego nie nakazuje (mało tego! często nawet nie ostrzega!), ale lepiej się tego trzymać, bo skutki zapomnienia o tym - jak widzieliśmy - mogą nie być przyjemne (wystarczy oczywiście zadeklarować to w klasie podstawowej).
  2. Jeśli klasa jest polimorficzna, to z reguły wszystkie jej metody, które mają być dostępne do przedefiniowania przez klasę dziedziczącą (!) powinny być wirtualne; te, które nie mają być dostępne, mają być prywatne. To oczywiście zależy jeszcze od tego, do czego dana metoda jest przeznaczona i czy może ona być zależna od wszelkich "nadbudówek" w klasach pochodnych (jeśli nie, wtedy może być zwykła i nawet inline, ale wtedy nie należy jej przedefiniowywać w klasie pochodnej!). Ale metoda wirtualna to jest taki "slot" obiektu, w który użytkownik klasy może sobie wsadzić coś innego, niż tam siedzi domyślnie (lub nie siedzi nic, jeśli to metoda czysto-wirtualna). W klasie polimorficznej zatem bezwzględnie dziel metody na takie, które mogą być takimi właśnie "slotami", i takie, których zmiany implementacji nie przewidujesz. W przypadku metod wirtualnych prywatność nie ma nic do rzeczy, bo prywatne metody wirtualne także można przedefiniować.
  3. Podczas przygotowania projektu, starannie określaj hierarchię klas. Jeśli stosujesz dziedziczenie, bądź pewien, dlaczego tak robisz i po co: czy tylko po to, żeby skrócić pisanie, czy po to, żeby wykorzystać właściwości obiektowe, może także polimorfizm?

Padło kiedyś takie pytanie, czy wszystkie metody powinny być wirtualne (dajmy na to, że projektant zdecydował, że klasa ma być polimorficzna). Odpowiedź - w dużym uproszczeniu, bo tylko tyle jestem w stanie na ten temat powiedzieć - mogę dać taką: te metody powinny być wirtualne, których sprecyzowanie w klasie podstawowej mogłoby być niewystarczające, bądź też jest po prostu niemożliwe. Nie wszystkie jednak muszą takie być - często można wskazać takie metody w klasie podstawowej, które w każdej z klas pochodnych z założenia będą wykonywały się identycznie. Jednak zupełnie inaczej do tej kwestii należy podchodzić w zależności od tego, czy projektuje się program, czy też bibliotekę. W programie bowiem możesz sobie dowolnie zmienić cokolwiek, jeśli uznasz to coś za niewłaściwie zaprojektowane (sam jesteś użytkownikiem klasy, którą projektujesz). W bibliotece niestety jest gorzej; tam właśnie najtrudniej jest przewidzieć, jak jakiś "szalony programista" zechce wykorzystać daną klasę. A właśnie możliwości w wykorzystaniu danej biblioteki głównie świadczą o jej funkcjonalności. W bibliotekach zwłaszcza zatem ważne jest, by "odpowiednie" metody były wirtualne, tak żeby użytkownik mógł sobie zmienić jej definicję w swojej klasie. Ale to ma też i złe strony: przecież po to w końcu mamy to programowanie obiektowe, żeby nakierować użytkownika na poprawny sposób programowania. Robienie wszystkich metod jako wirtualne może spowodować, że użytkownik będzie mógł przedefiniować każdą metodę w swojej klasie pochodnej i w ten sposób rozwalić całą spójność imperatywną klasy podstawowej (w C++ nie istnieje metoda na uniemożliwienie dziedziczenia ani przysłaniania metod, jak np. "final" w Javie – ale też nie każda metoda musi być wirtualna).

Nie można też zapominać, że przesadzenie z funkcjami wirtualnymi może paskudnie spowolnić wykonywanie kodu. Przykład? Mamy zdarzenia, które są odbierane w kolejce komunikatów i na każdy typ zdarzenia mamy osobną metodę wirtualną. "Domyślnie", czyli w klasie podstawowej jest to zdefiniowane tak, że nie dzieje się nic. Użytkownik może sobie zdefiniować metodę odpowiadającą jakiemuś tam zdarzeniu w jakiś konkretny sposób. Reszte pozostawi jak było. I co teraz? I teraz na każde zdarzenie, które nadejdzie, wywołuje się odpowiednią metodę wirtualną, czyli każde zdarzenie zostanie obciążone rozkazami wywołania i powrotu, nawet jeśli nie jest związane z wykonaniem jakiejś konkretnej czynności. Jak widać, w tym przypadku stosowanie metod wirtualnych nie jest najlepszym pomysłem.

Co prawda programowanie obiektowe – bardzo rozreklamowane w ostatnim czasie dzięki również C++, który akurat do programowania obiektowego nie jest najlepszym językiem, jak również Javę, która też niewiele więcej od C++ do programowania obiektowego wnosi – jest już teraz nie tylko stare, ale i pewnie spowszedniałe, jednak wciąż znajduje zastosowanie. Jeśli jednak ktoś tą technikę dopiero usiłuje poznać – proszę z radości nie klaskać uszami! Programowanie obiektowe to tylko jedna z (bardzo wielu) technik programowania; nie jest lekarstwem na wszystkie problemy, a jedynie posiada pewną liczbę zastosowań. Nie jest też nawet czymś specjalnie wymyślnym, czy skomplikowanym; jest to jedna z technik, podobnie jak programowanie proceduralne, czy funkcjonalne.

W programowaniu obiektowym warto przede wszystkim wiedzieć o dwóch sprawach. Po pierwsze, jak wiemy, język C++ jest językiem o ścisłej typizacji. Smalltalk nie ma żadnej. I tu niestety nie jest to fakt bez znaczenia. Właśnie zastosowanie właściwości obiektowych wymaga od użytkownika dobrowolnego zrzeczenia się typizacji. Oczywiście nie w pełni, lecz w konkretnie zdefiniowanym zakresie. Mając klasę bazową X (abstrakcyjną) i wyprowadzając z niej klasy A, B i C, przez cały czas operujemy obiektami tych trzech klas za pośrednictwem wskaźnika (lub referencji) do X. Nie zawsze oczywiście, ale bez tego nie byłoby efektu. :) Możemy np. mieć zbiornik, który przechowuje wskaźniki do X, które wskazują na obiekty tych trzech typów. Obiekty ze zbiornika pobieramy jako wskaźniki do X i z punktu widzenia typizacji C++ nie wiemy o tych obiektach nic ponad to, że są one klasy wyprowadzonej z X. Co za tym idzie, nie będziemy w stanie wykonać na nich żadnej metody, która nie jest zdefiniowana dla typu X, nawet jeśli jest dostępna dla typów A, B lub C. Owszem, to jest ograniczenie – no ale przecież w klasie X możemy sobie zrobić dowolne metody wirtualne, a w ostateczności można też skorzystać z dynamic_cast. Jednak odgadnięcie typu faktycznego obiektu (szczerze to należy w programowaniu obiektowym unikać takich rzeczy – nie po to ono istnieje!), jak również samo wywołanie metody wirtualnej posiada pewien narzut. W rzeczywistości nie jest on wielki. Pomyśl też, że gdyby nie to, nie byłoby możliwe trzymanie w jednym zbiorniku obiektów różnych typów!

Do czego prowadzi polimorfizm?

Druga sprawa, którą tu chciałem poruszyć, to kwestia sposobu realizacji tego tzw. polimorfizmu, zasadzającego się w C++ na metodach wirtualnych. Otóż, jak się można domyślać, większość implementacji stosuje tablicę ze wskaźnikami do metod, do której wskaźnik jest zawarty w obiekcie. Ale to tylko od tej strony wszystko jest oczywiste. Załóżmy, że dysponujesz klasą dostarczoną przez bibliotekę (czy po prostu przez kogoś innego), jak i zestawem funkcji, które na obiektach tej klasy operują. Ty dostarczasz do tej operacji utworzony przez siebie obiekt tej klasy, a owe funkcje będą potem temu obiektowi wywoływać metody. Załóżmy, że chciałbyś zmienić zachowanie się niektórych metod (i w efekcie działania owych funkcji). Co więc możesz zrobić? Wyprowadzić swoją klasę na bazie tamtej i samemu zdefiniować odpowiednie metody wirtualne, a funkcje, którym ten swój obiekt podasz wywołają wtedy te nowo zdefiniowane.

Ogólnie więc, jest to niejako odwrócenie do góry nogami programowania proceduralnego. To już nie ty wywołujesz funkcję. Ty ją tylko definiujesz i dostarczasz, a wywołuje ją kto inny. Technika ta występuje w bardzo wielu mutacjach (polimorfizm jest jedną z nich) i nazywa się "callback"; polega właśnie na dostarczeniu funkcji (najczęściej wskaźnika), którą potem ktoś (w "stosownym" dla siebie momencie) wywoła.

Tu chciałbym też zwrócić uwagę na parę spraw. Że nie dać się zwariować to już chyba za często powtarzam :). Chcę jednak przede wszystkim zaznaczyć, że w technikach obiektowych jak rzadko gdzie bardzo istotne znaczenie ma dobra, rzetelna dokumentacja, jak również mocne i zaawansowane narzędzia do debugowania. O ile w programowaniu proceduralnym można wiele wyśledzić ręcznie, oglądając kod, o tyle oglądając kod obiektowy można tylko oczy wytrzeszczać i nic się nie zobaczy. Nawet jak się zobaczy wywołanie metody, to jeszcze nie wiadomo, która z nich się wywoła i co będzie dalej, bo o większości rzeczy decyduje wiele czynników dopiero podczas działania prgogramu (z poprzedniego akapitu możesz się też zorientować, że efektywnie można niejako zmieniać zachowanie się istniejących już funkcji, choć ich kod pozostaje nietknięty i nawet może nie być do niego źródeł!). Przestrzegam też przed zbyt hurraoptymistycznym używaniem enkapsulacji (jakąkolwiek metodą). Prowadzi to bardzo często do zaciemniania informacji i koszmarów podczas debugowania. Nie zawsze rozdzielanie różnych elementów kodu od siebie (na zasadzie chowania się za zasłonę) jest sensowne; bardzo często stanowi utrudnianie sobie życia. Owo rozdzielanie trzeba robić w starannie dobranych miejscach; jest ono bowiem zarówno pożądane w pewnych sytuacjach (aby zmniejszyć stopień skomplikowania pewnych tworów programistycznych, jak również zmniejszenia wzajemnych zależności), jak i często wielce niepożądane (potrafi siać niewypowiedzianą dezinformację, a w C++ często też konieczność rzutowania – oczywiście przez reinterpret_cast).

Jedną z technik, gdzie takie oddzielanie dobrze się sprawdza jest inna mutacja techniki "callback", zwana technologią sygnałów-slotów. Z powodzeniem znalazła zastosowanie w bibliotekach Qt i GTK+ (wraz z GTK--); jest też dostępna oddzielna biblioteka zwana libsigc++ (która pochodzi właśnie z GTK--). Jest też dużo bogatsza, bardziej ogólna, bezpieczniejsza i szybsza od jej odpowiednika w Qt (gdyż jest oparta na wzorcach). Właśnie w dokumentacji do Qt autorzy piszą coś takiego: "Żaden obiekt nie wie o drugim. To jest esencja programowania komponentowego.". Otóż to wcale nie fakt, że obiekty o sobie nic nie wiedzą, jest ważny (ani nie jest bynajmniej zaletą). Ważne jest to, że do używania sygnałów-slotów w Qt żaden obiekt NIE POTRZEBUJE wiedzieć niczego o drugim obiekcie. Rozdzielenie czynności na sygnały i sloty z kolei jest zaletą: pozwala na rozgraniczenie dwóch rzeczy logicznie i koncepcyjnie, dzięki czemu unika się mieszania dwóch odrębnych elementów programowania. Alternatywne techniki wymagają ich współistniejących tylko dlatego, że mieszają w sobie nawzajem definicjami.

Hierarchizacja, czy polimorfizm?

Chciałbym na zakończenie poruszyć pewien bardzo ważny aspekt, mianowicie kwestię tworzenia hierarchii klas. Wielu ludzi zbytnio przywiązanych do matematyki chciałoby, żeby hierarchia klas w języku obiektowym wyglądała tak, jak hierarchia zdefiniowana w matematyce. I tu okazuje się, że - niestety - określenie hierarchii w programowaniu obiektowym jest implementacyjne i nie musi odpowiadać hierarchii jaka została dobrana do podobnych pojęć w matematyce, gdzie owo określenie hierarchii jest czysto abstrakcyjne. Doskonale w takich wypadkach widać, jak bardzo upośledzony jest ludzki umysł, skoro nawet hierarchizację musi uprościć do granic możliwości, by móc ją zrozumieć. Oto znana z matematyki hierarchia pojęć od czworokąta do kwadratu:

(Quadrangle) (Trapeze) (Parallel) (Diamond) (Square) Czworokąt -> Trapez -> Równoległobok -> Romb ---> Kwadrat `-> Prostokąt (Rectangle)

Tutaj `Kwadrat' jest najbardziej wyjściową i najbardziej `specyficzną' klasą: w matematyce mówimy, że kwadrat jest prostokątem i trapez jest czworokątem. Niestety, próba przełożenia tej hierarchii na dowolny język obiektowy spowoduje powstanie dziwadła. W hierarchii matematycznej bowiem (przynajmniej tej) istotną rolę pełni dziedziczenie i dokładanie następnych założeń. W efekcie więc dziedziczenie stanowi stopniowe OGRANICZANIE możliwości obiektów. W językach programowania obiektowego niestety jest dokładnie odwrotnie - stanowi to ROZSZERZANIE możliwości obiektów. Zatem w C++ owa hierarchia będzie wyglądała następująco:

class Square {
protected:
  float l; // kwadrat ma tylko jedną daną - długość boku
public:
  float& L() { return l; }
};

class Rectangle: virtual Square {
protected:
  float l2; // długość drugiego boku
public:
  float& L1() { return L(); }
  float& L2() { return l2; }
};

Oczywiście nie będę dodawał później metod w stylu `L', żeby nie gmatwać. Proszę jednak pamiętać, że takie metody powinny istnieć.

class Diamond: virtual Square {
  protected:
  float slope; // nachylenie albo wysokość, jak się zrobi tak się zrobi
};

class Parallel: Rectangle, Diamond {
};

class Trapeze: Parallel {
  protected:
  float l3;
};

class Quadrangle: Trapeze {
};

W tym ostatnim oczywiście nie dodaje się żadnych pól, tylko pola zmieniają znaczenie (tzn. metody operujące na nich wykorzystują je w inny sposób). Ponieważ pola są tylko szczegółem implementacyjnym, a wszelkie operacje na obiektach tych typów są wykonywane wyłącznie przez ich metody wirtualne, więc użytkownik obiektu i tak nie widzi różnicy i może wykonać jakąś metodę na obiekcie - jak mniema - trapezu, który może być w rzeczywistości obiektem zarówno trapezu, jak i czworokąta.

Przykład ten jest całkiem abstrakcyjny i miał być, ale niestety nie jest "całkiem obiektowy" (chociaż wszelkie dziedziczenia są prywatne!). Dlaczego pola są w sekcji chronionej? W sumie nie muszą być, przynajmniej nie wszystkie. Teoretycznie jest możliwe, żeby klasa dziedzicząca zamiast z `l1' korzystała z `L1()' i pozostawiła obsługę `l1' klasie, która to zadeklarowała. Jak jednak byłoby możliwe wtedy, żeby klasa Quadrangle inaczej korzystała z pól, niż klasa Trapeze? Twórca klasy Quadrangle musiałby zdrowo się napocić, żeby "nagiąć" klasę Trapeze do robienia różnych rzeczy tak, jak tego wymaga Quadrangle. Nie mówię wcale, że nie jest to możliwe, tylko po prostu uciążliwe: Quadrangle wymagałby dodania nowego pola opisującego wielkość czwartego boku, ale po jego dodaniu istnienie pola Trapeze::slope przestaje mieć sens. Pisałem wcześniej o kwestii "zasłaniania wnętrza" klasy. Jeżeli piszemy klasę, którą bezpośrednio udostępniamy, to oczywiście, że powinna mieć zasłonięte szczegóły implementacyjne. Ale jeśli ktoś sam pisze całą hierarchię, to chyba ma "do samego siebie" zaufanie, że "wie, co robi" grzebiąć w "szczegółach implementacyjnych" (chyba, że ma rozdwojenie jaźni, ale wtedy nie powinien zajmować się programowaniem obiektowym, tylko zgłosić do psychiatry :*).

Operatory RTTI

Jeszcze przypomnę tutaj kwestię operatorów RTTI. Są to konkretnie – poznany już – typeid, oraz dynamic_cast. Jak dziala typeid, to już mówiłem, ale nie mówiłem, jak on działa dla typów polimorficznych. Otóż właśnie jeśli obiekt jest typu polimorficznego, to operator typeid pozwala stwierdzić RZECZYWISTY typ danego obiektu, zwracając referencję do struktury identyfikującej. Zatem:

int main() {
  try {
    ...
  }
  catch ( exception& e ) {
    cerr << typeid (e).name() << endl;
  }
  return 0;
}

Ten przechwycony wyjątek (zakładając, że był klasy exception) może być różnych typów (z hierarchii standardowych wyjątków). Ten kawałek kodu pozwala nam stwierdzić jego rzeczywisty typ (ale oczywiście przypominam, że standard nie precyzuje, jak ta nazwa typu ma zostać wyrażona).

Do czego z kolei służy dynamic_cast? Do obsłużenia sytuacji, z jaką mamy do czynienia w "czystym" programowaniu obiektowym. Robi właściwie to samo, co static_cast, ale w odróżnieniu odeń robi to ze sprawdzaniem poprawności. Oczywiście nie muszę chyba tłumaczyć, że takie sprawdzenie można wykonać tylko dla typów polimorficznych. Operator ten służy więc również do sprawdzania, czy pewien obiekt jest rzeczywiście danej klasy. Rzutować można wskaźnik lub referencję. Jeśli rzutowanie się nie powiedzie, to przy wskaźniku zwracany jest wskaźnik zerowy, przy referencjach – rzuca się wyjątkiem bad_cast.

Operator ten ma dość poważne zastosowanie. Jeśli dysponujemy np. w jakimś zbiorniku obiektami pewnej abstrakcyjnej klasy, ktora ma nawet tylko dwie pochodne, to tworzenie wszystkich ich metod jako wirtualnych nie byłoby specjalnie rozsądne. Jeśli np. w konkretnej funkcji (która ma pracować wspólnie dla obu tych typów) pewien fragment kodu ma dotyczyć tylko gdyby obiekt był jednego z nich, to akurat najlepiej jest zastosować tutaj ten operator, w szczególności np. tak:

int Fn( Pojazd* x ) {
  ...
  Samochod* s = dynamic_cast<Samochod*>( x );
  if ( s ) {
    ... // kod specyficzny dla Samochod
  }
  ...
}

Operator dynamic_cast działa takoż na styku dziedziczenia wirtualnego. Tak się bowiem składa, że podobiekt bazowy ma informacje o typach nad-obiektów, więc tu nie ma problemu.