3. Dziedziczenie i polimorfizm

3.4. Hierarchia klas

Hierarchia klas jest zbiorem klas połączonych dziedziczeniem, dzięki któremu tworzą uporządkowaną strukturę. 

Dzięki hierarchii można zapisać w kodzie hierarchiczne zależności między bytami. Typowe przykłady już widzieliście wcześniej: "Prostokąt jest rodzajem figury nieobrotowej, która jest rodzajem figury", czy też „Wóz strażacki jest rodzajem ciężarówki, która jest rodzajem pojazdu”. Realnie spotykane hierarchie są ogromne, i rozbudowane zarówno wszerz (wiele typów na tym samym poziomie), jak i w głąb (wiele poziomów dziedziczenia).

Jeśli budujecie własną hierarchię - często na jej szczycie warto umieścić interfejsy - ułatwi to minimalizację zależności między korzystającymi z obiektów w hierarchii a ich implementacją. 

W zasadzie w dobrze zaprojektowanych hierarchiach rzadko spotyka się sytuacje, w których chcemy poznać rzeczywisty typ wskazywanego obiektu. Czasem jest to jednak konieczne. Popatrzcie raz jeszcze na przykład figur z poprzedniego rozdziału. Jeśli chcielibyśmy obrócić figurę, korzystając ze wskaźnika na klasę bazową, to nie mamy takiej możliwości: 


void przesunIObroc(ICFigura* co, int gdzieX, int gdzieY, int oIle) {
   co->przesun(gdzieX, gdzieY); 
   // nie da się wywołać obróć - nie ma takiej metody w def. interfejsu
   // co->obroc(oIle); 
}

Możemy natomiast zauważyć, że obracanie figury obrotowej nie ma większego sensu - o ile byśmy jej nie obrócili, i tak będzie wyglądała tak samo. Więc - implementacja tej metody ma sens, jeśli jesteśmy w stanie sprawdzić, czy co wskazuje na CFiguraNieobrotowa lub jej pochodną. Możemy to zrobić korzystając z dynamic_cast:


void przesunIObroc(ICFigura* co, int gdzieX, int gdzieY, int oIle) {
   co->przesun(gdzieX, gdzieY); 
   // nie da się wywołać obróć - nie ma takiej metody w def. interfejsu
   if (auto nco=dynamic_cast<CFiguraNieobrotowa*>(co)) {
      // tu już wiemy że jest to figura nieobrotowa
      nco->obroc(oIle); 
   }   
}

Operator rzutowania dynamic_cast sprawdza, czy dany obiekt jest jednocześnie obiektem typu na który rzutujemy, i - jeśli tak, zwraca wskaźnik na ten obiekt. Jeśli nie jest - zwraca wartość nullptr. 

W przypadku chęci wykorzystania dynamic_cast do uzyskania referencji do obiektu - to także jest możliwe - tyle że w tym wypadku nie bardzo wiadomo jak odebrać informację o tym że rzutowanie nie powiodło się. C++ w takim wypadku rzuci wyjątek: 


CKolo k; 
// poniższa linijka rzuci wyjątek std::bad_cast
CFiguraNieobrotowa& f = dynamic_cast<CFiguraNieobrotowa&>(k); 

Tyle że przekazywanie informacji w normalnym toku wykonania przez wyjątki nie powinno następować - dlatego też nie  jest to przykład sensownego wykorzystania rzutowania na referencję

Rzutowanie na referencję wykonujcie tylko wtedy, kiedy inny typ niż oczekiwany jest nieakceptowalny i nie ma prawa się zdarzyć. 
W ogóle nadużywanie operatora dynamic_cast jest nieeleganckie. Na wstępie do tego rozdziału napisałem - że trzeba go unikać. Lepiej jest tak zaprojektować swoje interfejsy, by rzutowanie nie było potrzebne. W moim przykładzie figur - wystarczy dodać do ICFigura wirtualną metodę obroc o pustej implementacji ... 

Przechowywanie obiektów z hierarchii w kontenerach STL

Pozostało mi w tym rozdziale zwrócić Wam uwagę na jeszcze jeden szczegół. Mamy pewien problem. Skoro z jednej strony typów abstrakcyjnych nie powinienem bezpośrednio kopiować (bo kopia może być niepełna), a z drugiej strony - kontenery STL przejmują obiekty na własność, a więc wykonują albo kopię, albo przeniesienie obiektu do kontenera - to jak umieszczać niejednorodny typ bazowy w kontenerze? 

Rozwiązania są dwa. Mało eleganckie jest umieszczanie w kontenerze wskaźników na typ bazowy: 


std::vector<ICFigura*> figury; 

Wadą takiego podejścia jest konieczność ręcznego usuwania obiektów, które są usuwane z wektora. Łatwo się pomylić, i łatwo doprowadzić do wycieków pamięci. Lepsze jest stosowanie inteligentnych wskaźników:


std::vector<std::unique_ptr<ICFigura> > figury; 
teraz - po usunięciu obiektu z wektora, zostanie on także skasowany.