Podręcznik
3. Dziedziczenie i polimorfizm
3.4. Hierarchia klas
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ę
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.