Czy wiesz, że jesteśmy również na Slacku? Dołącz do nas już teraz klikając tutaj!

Dynamic cast oraz type id jako narzędzia RTTI


2019-06-20, 00:00

Jak zapewne wiecie, istnieją pewne mechanizmy w C++, które pozwalają na uzyskanie danych o typie obiektu w czasie życia programu. Być może zastanawiacie się po co są one w C++. Dzięki nim możemy np. obsługiwać wyjątki oraz implementować typy takie jak std::any. W tym artykule przyjrzymy się narzędziom dynamicznego rzutowania oraz uzyskiwania informacji o dynamicznym typie zmiennej.

Obsługa wyjątków w C++ nie byłaby możliwa bez RTTI (ang. run time type information), ponieważ nie moglibyśmy dopasować typu rzuconego wyjątku do odpowiedniej klauzuli catch. Rzutowanie obiektu any na typ docelowy sprawdza, czy rzutujemy na poprawny typ (może on być różny w różnych uruchomieniach programu), stąd bez RTTI bezpieczne zaimplementowanie typu any byłoby niemożliwe.

Jeszcze innym zastosowaniem mechanizmu RTTI jest sprawdzenie, czy referencja lub wskaźnik na klasę bazową wskazuje na konkretny typ klasy pochodnej. Oczywiście można by dyskutować, czy używanie tego typu rzutowania jest dobrym rozwiązaniem rozważając architekturę programu. Celem artykułu jednak jest zaznajomienie się ze szczegółami działania mechanizmów rzutowania dynamicznego oraz informacji o typie a nie dyskusje na temat architektury oprogramowania.

Dynamic cast

Mechanizmy RTTI działają tylko dla tych typów, które są typami polimorficznymi. Typy polimorficzne to takie typy, które mają przynajmniej jedną funkcję wirtualną lub przynajmniej jedną taką funkcję dziedziczą po rodzicu (typy oznaczone jako final, również należą do tego grona). Jednak dynamic_cast nie jest narzędziem zawsze korzystającym z RTTI. W pewnych warunkach zachowuje się tak samo jak static_cast. Możemy się o tym przekonać analizując poniższy przykład.

struct B {};
struct D : B {};

int main(){
    B* ptr = new D;
    dynamic_cast<D*>(ptr);
}

Intencją programu jest zrzutowanie wskaźnika na klasę bazową na wskaźnik do obiektu klasy pochodnej. Tego typu rzutowanie zdecydowanie wymaga (aby było bezpiecznie) RTTI. W końcu nie jesteśmy w stanie wywnioskować, czy rzutowanie jest poprawne nie wiedząc w jaki sposób utworzony został wskaźnik ptr. Niestety jednak typ B nie jest polimorficzny, a więc sprawdzenie, czy rzutowanie jest poprawne jest niemożliwe i dostaniemy błąd kompilacji, który może brzmieć następująco:

 error: 'B' is not polymorphic

Spróbujmy w takim razie zrobić odwrotne rzutowanie, czyli z klasy pochodnej na klasę bazową:

struct B {};
struct D : B {};

int main(){
    D* ptr = new D;
    dynamic_cast<B*>(ptr);
    delete ptr;
}

Okazuje się, że takie rzutowanie jest jak najbardziej poprawne i zachowuje się tak samo jak static_cast. Jest to ten z przypadków, w których dynamic_cast nie potrzebuje mechanizmu RTTI do poprawnego działania. Możemy więc powiedzieć, że dynamic_cast to narzędzie do poruszania się wzdłuż drzewa dziedziczenia. To, czy dynamic_cast będzie potrzebować RTTI do działania jest zależne od tego, czy RTTI jest wymagane do jego poprawnego działania w konkretnym przypadku. Co więcej, CppCoreGuidelines zaleca właśnie używanie dynamic_cast do rzutowania obiektu klasy bazowej na pochodną zamiast static_cast.

Jako ogólną zasadę programowania powinniśmy przyjąć, że jeżeli chcemy poruszać się po drzewach dziedziczenia, to powinniśmy używać dynamic_cast lub rzutowania niejawnego. Jeżeli natomiast chcemy używać konstruktorów konwertujących, czy operatorów rzutowania, to powinniśmy używać static_cast. Dlaczego tak?

Przede wszystkim dla czytelności, jeżeli przyjmiemy za zasadę, że do rzutowania wzdłuż drzewa dziedziczenia będziemy używać dynamic_cast to łatwiej będzie nam czytać kod. Oczywiście będziemy również bezpieczni jeżeli spróbujemy rzutować obiekt typu bazowego na pochodny, a rzutowanie takie nie będzie możliwe. Dynamic_cast po prostu zwróci nam w takim przypadku pusty wskaźnik.

Przeanalizujmy następujący przykład:

struct B {virtual void foo();};
struct C : B {void foo() override; void special_foo();};
struct D : B {void foo() override; void even_better_special_foo();};
/* any other types*/

void call_best(/*args*/){
  /**/
}

Mamy więc klasę bazową z funkcją foo oraz przynajmniej 2 implementacje tej klasy. Mamy również funkcję call_best, która będzie starać się wywołać jak najlepszą funkcję foo ze wszystkich możliwych. Tak więc chcielibyśmy, żeby dla obiektu typu C wywołana została funkcja special_foo, a dla typu D even_better_special_foo.

Jakie argumenty musimy przekazać do funkcji call_best, żeby program działał prawidłowo? Mamy dwie opcje (które przychodzą mi do głowy). Pierwsza opcja (niepolecana) to przekazać wartość unikalną dla danego typu np. jakiś enumerator oraz wskaźnik na klasę bazową. W ten sposób będziemy mogli zaoszczędzić sprawdzenie czasu uruchomienia (ang. runtime check):

enum class Type{B, C, D};
void call_best(B* ptr, Type type){
  if(type == Type::C)
    static_cast<C*>(ptr)->special_foo();
  else if (type == Type::D)
    static_cast<D*>(ptr)->even_better_special_foo();  
  else
    ptr->foo();
}

Oczywiście aplikacja będzie działać poprawnie, ale pewnym kosztem. Przede wszystkim będziemy musieli pilnować, aby zmienna type miała odpowiednią wartość. Podanie złej wartości poskutkuje zasmakowaniem undefined behavior. Problem ten można wyeliminować, jeżeli typowi B dodamy funkcję Type type();. Wtedy jednak wszystkie implementacje klasy B będą musiały wiedzieć o sobie nawzajem (wcześniej ten problem dotyczył wyłącznie funkcji call_best i jej wywołujących). Oba podejścia mają swoje minusy.

Jeżeli jednak do rzutowania będziemy używać dynamic_cast, to nie będziemy musieli pamiętać o poprawnym przekazywaniu wartości enum’a ani też nie związujemy mocno wszystkich implementacji ze sobą. Spójrzmy na przykład:

void call_best(B* ptr){
  if(C* c; c = dynamic_cast<C*>(ptr))
    c->special_foo();
  else if (D* d; dynamic_cast<D*>(ptr))
    d->even_better_special_foo();
  else
    ptr->foo();
}

W tym wypadku nie ma mowy o pomyłce, ponieważ sprawdzanie robi za nas sam kompilator (który prawdopodobnie ma dynamiczne rzutowanie bardzo dobrze przetestowane). Nie musimy również w klasie D wiedzieć o istnieniu klasy C i vice versa.

Dynamic cast, którego (prawdopodobnie) nie znałeś

Jeżeli chodzi o dynamic_cast, to oczywiście (jak wszystko w C++) jest to bardzo elastyczne narzędzie. Większość osób (moim przypuszczeniem) nie zna pełni możliwości dynamicznego rzutowania, ponieważ albo nigdy takiej funkcjonalności nie potrzebowali, albo nigdy po prostu nie było dane im poznanie jakiegoś konkretnego zachowania.

Rzutowanie dynamic_cast<void*>

Jak już wspomnieliśmy, dynamic_cast służy do poruszania się po drzewie dziedziczenia. Jednym z takich ruchów po drzewie jest przejście do najbardziej pochodnego obiektu, posiadając wskaźnik na obiekt jednej z klas bazowych. Aby dostać wskaźnik do takiego obiektu należy jako typ funkcji dynamic_cast przekazać wskaźnik na void'a.

Prawdopodobnie najlepiej będzie to widać na przykładzie:

struct B {int a; int b; virtual ~B()=default;};
struct C {int a; int b; virtual ~C()=default;};
struct D : C, B {int c; int d;};


int main(){
    D* ptrd = new D;
    B* ptrb = ptrd;
    C* ptrc = ptrd;

    assert(dynamic_cast<void*>(ptrb) == ptrd);
    assert(dynamic_cast<void*>(ptrc) == ptrd);
    delete ptrd;
}

Jak więc widać na przykładzie, mając obiekt klasy pochodnej oraz wskaźnik na jego bazowy pod-obiekt, możemy użyć narzędzia dynamic_cast, aby dostać wskaźnik na obiekt najbardziej pochodny. Oczywiście sprawdzenie, który obiekt jest najbardziej pochodny wymaga wykorzystania mechanizmu RTTI, gdyż z obiektu bazowego chcemy dostać się do pochodnego, a nie odwrotnie.

Jednocześnie zarówno klasa B, jak i klasa C posiadają wirtualny destruktor. Jest on potrzebny, aby mechanizm RTTI miał szansę zadziałać (bez żadnej funkcji wirtualnej dostaniemy po prostu błąd kompilacji).

Niejednoznaczne rzutowanie

Do tej pory rozpatrywaliśmy proste przypadki dziedziczenia - np. dziedziczenie po dwóch różnych klasach pochodnych. A co jeżeli nasz obiekt pochodny będzie zawierać w sobie dwa obiekty klasy, na którą będziemy chcieli zrzutować dany wskaźnik lub referencję?

Wyobraźmy sobie dwa przykłady drzewa dziedziczenia:

struct B {int a; int b; virtual ~B()=default;};
struct C : B {int a; int b; virtual ~C()=default;};
struct D : C, B {int c; int d;};

W powyższym przykładzie obiekt klasy D będzie posiadał dwa obiekty klasy B. Jeden bezpośrednio poprzez dziedziczenie po B, a drugi pośrednio poprzez dziedziczenie po C. Jeżeli teraz spróbujemy zrzutować obiekt klasy D na obiekt klasy B, dostaniemy błąd kompilacji.

error: ambiguous conversion from derived class 'D' to base class 'B':
    struct D -> struct C -> struct B
    struct D -> struct B
    B* ptrb = dynamic_cast<B*>(ptrd);

Niejednoznaczność może nastąpić jednak również w drugą stronę. Rozpatrzmy następujący schemat dziedziczenia:

struct A{virtual ~A()=default;};
struct B{virtual ~B()=default;};
struct C : A, B{};
struct D : C, B{};

W tym przykładzie klasa D będzie miała dwa podobiekty klasy B oraz tylko jeden podobiekt A. W tym przypadku:

int main(){
    D* d = new D;
    A* a = dynamic_cast<A*>(d); // jest ok, tylko jeden obiekt bazowy A
    B* b= dynamic_cast<B*>(a); // istnieją 2 obiekty bazowe B. rzutowanie niejednoznaczne
    assert(b == nullptr);
    delete d;
}

W tym wypadku oczywiście nie otrzymamy błędu kompilacji z powodu niejednoznacznego rzutowania, ponieważ rzutowanie to może być sprawdzone dopiero podczas wykonywania się aplikacji. W przypadku niejednoznacznego rzutowania z obiektu klasy bazowej na obiekt klasy pochodnej dostaniemy po prostu w wyniku nullptr.

Typeid

Mam nadzieję, że chociaż trochę udało mi się Was zaskoczyć działaniem tak dobrze znanego wyrażenia jak dynamic_cast. Teraz nadszedł czas na inne narzędzie RTTI - typeid.

No więc jak zapewne Wam wiadomo typeid jest narzędziem, które pozwala na otrzymanie informacji o typie. Uzyskana informacja pozwala np. na uzyskanie reprezentacji typu w postaci tekstu, ale od początku.

Aby w ogóle używać typeid należy najpierw załączyć nagłówek <typeinfo>. Wynika to z tego, że wynikiem wyrażenia typeid jest referencja do obiektu type_info, który jest zdefiniowany właśnie w tym nagłówku.

Typeid != RTTI

Typeid (podobnie jak dynamic_cast) nie jest narzędziem, które zawsze korzysta z mechanizmu RTTI do swojego działania. Jeżeli argumentem wyrażenia typeid jest niepolimorficzny type (nie dziedziczy po klasie bazowej, która ma wirtualne funkcje, ani też sama nie definiuje takich), to zwracana referencja do type_info będzie opisywała właśnie typ podanego argumentu, nawet jeżeli obiekt do którego odnosi się argument wskazuje na inny typ. Oczywiście najbardziej znaczącym i opisowym będzie po prostu kompilowalny przykład kodu:

#include <typeinfo>
#include <iostream>

struct A{};
struct B : A{};

std::type_info const& give_me_type(A&& a){
  return typeid(a);
}

int main(){
  auto& info = give_me_type(B{});
  std::cout << info.name() << std::endl;
}

W moim przypadku program wypisuje następujący komunikat:

1A

Łańcuch tekstu zwracany przez funkcję name może być różny w zależności od użytego kompilatora i zachowanie to jest jak najbardziej w porządku (jeżeli chodzi o zgodność ze standardem), więc “1” na początku nie powinno nas szczególnie dziwić.

Co ciekawe, w przypadku, kiedy argumentem typeid jest niepolimorficzny typ, to argument jest “nieewaluowany”, co znaczy, że wyrażenie nie jest wykonywane przez program. Co to oznacza w praktyce? Okazuje się, że możemy robić dereferencję pustego wskaźnika, a nasz program będzie zachowywać się poprawnie. Na przykład:

int main(){
  auto& info = typeid(*((B*)nullptr));
  std::cout << info.name() << std::endl;
}

powyższy program nie dość, że się skompiluje, to będzie on działał prawidłowo. Wyrażenie podane jako argument typeid nie jest wykonywane przez program, kompilator jedynie korzysta z typu, którego obiekt byłby wynikiem wyrażenia.

Innym ciekawym przykładem jest fakt, że lambdy nie mogą być używane (aż do standardu C++20) w nieewaluowanych kontekstach, w związku z czym wyrażenie

typeid([](){});

jest niepoprawne i otrzymamy błąd kompilacji. Od C++20 lambdy (więcej na temat lambd możecie znaleźć w książce C++ Lambda Story), które mają pustą listę przechwytywania mogą być używane w nieewaluowanych kontekstach i powyższe wyrażenie stanie się poprawne. Funkcja name może wtedy zwrócić np. następujący łańcuch znaków: Z4mainEUlvE_ (w przypadku kompilatora gcc10, gdzie lambda była tworzona w funkcji main).

Typeid(type-id)

Jeżeli argumentem typeid jest typ, a nie wyrażenie, to zachowuje się ono tak samo jak typeid zaaplikowane do niepolimorficznego typu. Możemy nawet powiedzieć, że typeid dla niepolimorficznego typu zostanie przetłumaczone jako:

typeid(decltype(<expression>))

, gdzie expression to wyrażenie zwracające niepolimorficzny typ.

Typeid == RTTI

Po omówieniu wyrażeń typeid dla typów niepolimorficznych, nadszedł czas na te polimorficzne.

W przypadku, kiedy typ wyrażenia jest polimorficzny, wyrażenie musi zostać obliczone, a na jego jego wyniku zostanie wykonany runtime check w celu otrzymania informacji o typie obiektu.

Jeżeli tak, to spodziewalibyście się, że następujący program będzie powodował undefined behavior:

#include <iostream>
#include <typeinfo>

struct A{virtual ~A() = default;};
struct B : A{};

int main(){
  auto& typeinfo = typeid(*((B*)nullptr));
  std::cout << typeinfo.name() << std::endl;
}

W końcu tym razem robimy dereferencję pustego wskaźnika i wyrażenie jest obliczane. Dlaczego więc nie napotykamy undefined behavior?

Dereferencja zerowego wskaźnika w typeid jest po prostu wyjątkowa i jeżeli spróbujemy zrobić dereferencję takiego wskaźnika to zamiast undefined behavior, wyrażenie typeid rzuci wyjątek typu std::bad_typeid lub pochodnego. Dotyczy to jednak wyłącznie wyrażenia będącego argumentem typeid, a nie jego podwyrażeń (częściowych wyrażeń składających się na pełne wyrażenie). Sprawdźmy to!

int main(){
  try{
    typeid(*((B*)nullptr));    
  } catch(std::bad_typeid& e) {
    std::cout << e.what() << std::endl;  
  }
}

Nie wiem jak u Was, ale u mnie wynikiem wykonania programu jest wypisanie tekstu: std::bad_typeid na standardowe wyjście programu. Tekst ten może się różnić w zależności od implementacji kompilatora lub biblioteki standardowej. Natomiast następujący program powoduje undefined behavior:

struct polymorph{virtual ~polymorph()=default;};
struct test{polymorph n;};

polymorph& getn(test& t){return t.n;}

int main()
{
   try{
    typeid(((test*)nullptr)->n);    
  } catch(std::bad_typeid& e) {
    std::cout << e.what() << std::endl;  
  }

}

W tym wypadku nadal mamy dereferencję pustego wskaźnika, ale nie zostanie rzucony wyjątek std::bad_typeid, ponieważ dereferencja ta nie jest najbardziej zewnętrznym wyrażeniem, a jedynie pod-wyrażeniem argumentu typeid. Program spowoduje undefined behavior (w moim wypadku np. nie wydarzyło się nic).

Wynik typeid

Wynikiem wyrażenia typeid jest referencja do stałego obiektu typu std::type_info lub pochodnego. Obiekt, który zostaje zwrócony z wyrażenia typeid żyje do końca życia programu, a więc nie musimy się obawiać, że referencja do tego obiektu w którymś momencie stanie się niepoprawna.

Interfejs type_info

Interfejs tego typu zawiera w sobie następujące funkcje:

  • operator równości i nierówności (bool operator==(const type_info& rhs) const noexcept;),
    która pozwala na sprawdzenie, czy dwa obiekty type_info wskazują na ten sam typ
  • funkcja before bool before(const type_info& rhs) const noexcept; która pozwala na uporządkowanie typów. Uporządkowanie typów jest zależne od implementacji, a może się zmienić nawet przy ponownym uruchomieniu programu
  • funkcja hash_code o prototypie size_t hash_code() const noexcept;,
    która zwraca hash dla danego typu. Zwrócony hash będzie unikalny dla danego typu, co powoduje, że będziemy mogli go używać np. jako klucz w kontenerze. Warto zwrócić uwagę, że hash_code może być różny dla tego samego typu podczas różnych wykonań programu.
  • funkcja name o prototypie const char* name() const noexcept, która zwraca tekstową reprezentację

Specyfikatory const i volatile

Być może narzuca Wam się pytanie “a co z typami const czy volatile ?”. Czy wynik typeid dla typów const i volatile jak i nie kwalifikowanych jest taki sam? Odpowiedź brzmi: tak. Typeid nie rozróżni nam typów bazując na ich specyfikatorach. Dla przykładu:

typeid(int) == typeid(const int); // zwraca true

W przypadku wskaźników będzie już jednak nieco inaczej. Co prawda wyrażenie:

typeid(int*const) == typeid(int*);

zwróci true, ponieważ const odnosi się do samego wskaźnika. Natomiast wyrażenie, gdzie wskaźnik nie będzie stały, a specyfikator odnosi się do wskazywanego obiektu już zachowa się inaczej. I tak:

typeid(int const*) == typeid(int*);

zwróci false.

Konwersje wyrażeń

Wyrażenia podane jako argument typeid nie ulegają konwersjom takim jak lvalue do rvalue, konwersja tablicy na wskaźnik oraz konwersja funkcji na wskaźnik. W praktyce spowoduje to, że otrzymamy informacje o precyzyjnym typie, który jest zwracany z wyrażenia. Na przykład mając funkcje:

void foo();
void bar(int a, int b);

możemy wypisać o typach następujące informacje:

std::cout << typeid(foo).name() << std::endl;
std::cout << typeid(void(*)()).name() << std::endl;
std::cout << typeid(bar).name() << std::endl;
std::cout << typeid(void(*)(int, int)).name() << std::endl;

Dokładny wypisany tekst jest zależny od implementacji kompilatora, ale tekst powinien różnić się dla poszczególnych wyrażeń typeid, gdzie argumentem jest wskaźnik na funkcję. W moim przypadku wypisane zostaje:

FvvE  # Funkcja zwracająca Void i przyjmująca Void
PFvvE # Wskaźnik (Pointer) na Funkcję zwracającą Void i przyjmującą Void
FviiE # Funkcja zwracająca Void i przyjmująca dwa obiekty typu Int
PFviiE # Wskaźnik (Pointer) na Funkcję zwracający Void i przyjmujący dwa obiekty typu Int

Limity RTTI

Język C++ nie byłby sobą, gdyby nawet w najbardziej popularnych narzędziach nie skrywał mrocznych zakamarków. Przyjrzymy się teraz przypadkom, gdzie używanie narzędzi RTTI nie tylko może spowodować błąd kompilacji, ale także undefined behavior.

Największym problemem jest używanie RTTI na obiektach, które są właśnie konstruowane lub niszczone. Zasada jest następująca: jeżeli w konstruktorze lub destruktorze użyjemy dynamic_cast lub typeid na obiekcie, który właśnie jest tworzony, to może on wskazywać na tworzony obiekt lub obiekt bazowy.

Prawdopodobnie najlepiej zilustruje to przykład zaczerpnięty bezpośrednio ze standardu:

struct V { virtual void f();};
struct A : virtual V {};
struct B : virtual V { B(V*, A*);};

struct D : A, B {
  D() : B((A*)this, this) { }
};

B::B(V* v, A* a) {
  //konstruktor B - obiekt D jeszcze nie jest utworzony!

  typeid(*this);                // poprawne: type_info dla B.
  typeid(*v);                   // poprawne: v wskazuje na obiekt typu V, który jest bazową B
  typeid(*a);                   // undefined behavior: typ A nie jest typem bazowym B
  dynamic_cast<B*>(v);          // poprawne: v wskazuje na obiekt typu V, który jest bazową B
  dynamic_cast<B*>(a);          // undefined behavior, a wskazuje na A, który nie jest bazową B
}

Jak więc widzimy, konstruktor B::B przyjmuje dwa wskaźniki na obiekty V i A, z czego typ B jest klasą pochodną V, a A jest oddzielnym typem niepowiązanym z B.

Klasa D dziedziczy po klasie A oraz B. Jako argumenty konstruktora klasy B przekazuje wskaźnik na siebie, czyli this. W związku z czym wewnątrz konstruktora obiektu typu B mamy pewność, że obiekt typu V został już w pełni utworzony. Nie mamy natomiast takiej pewności, jezeli chodzi o obiekt klasy A, a więc dynamic_cast oraz typeid spowoduje dereferencję wskaźnika na potencjalnie niezainicjalizowany obiekt, co skutkuje undefined behavior.

Typeid jako dynamic_cast

Być może zdarzyło się Wam słyszeć o tym, że typeid może służyć jako szybszy dynamic_cast. Zaimplementujmy sobie więc taką funkcję:

template <typename Dest, typename Src>
Dest* fast_dynamic_cast(Src* src){
  if(typeid(*src) == typeid(Dest)){
    return static_cast<Dest*>(src);
  }
  throw std::bad_cast();
}

Faktycznie będzie ona szybsza od zwykłego dynamic_cast’a w niektórych przypadkach. Jej użycie możemy zobaczyć na następującym przykładzie:

struct A {virtual ~A()=default;};
struct B : A{};

int main(){
    A* a= new B;
    fast_dynamic_cast<B>(a);
}

Program będzie działał prawidłowo i żaden wyjątek nie zostanie rzucony, ale…

Typeid nie zawsze może służyć do dynamicznego rzutowania

Wystarczy lekko zmodyfikować poprzedni przykład, żeby zobaczyć jaką wadę ma nasza funkcja fast_dynamic_cast :

struct A {virtual ~A()=default;};
struct B : A{};
struct C : B{};

int main(){
    A* a= new C;
    fast_dynamic_cast<B>(a); // uncaught exception
}

Powyższy program nie zakończy się sukcesem, ponieważ nie zostanie złapany wyjątek std::bad_cast. Pozostaje pytanie: dlaczego tak się stało?

Dzieje się tak, ponieważ dynamic_cast przeszukuje całe drzewo dziedziczenia w celu sprawdzenia, czy rzutowanie jest poprawne. W związku z tym znajdzie informację, że jednym z typów w drzewie dziedziczenia jest typ B. Wyrażenie typeid natomiast zwraca informację wyłącznie o obiekcie najbardziej pochodnym z całego drzewa. Stąd wariant z użyciem typeid jest szybszy, ale jego wynikiem nie zawsze będzie to, czego się spodziewamy. Dlatego właśnie typeid może być używane w takich sytuacjach wyłącznie wtedy, kiedy typ na który rzutujemy jest statycznym typem obiektu, który przekazujemy do funkcji fast_dynamic_cast. Stąd prawdopodobnie nazwa tej funkcji powinna prawdopodobnie być zmieniona na most_derived_cast.

Podsumowanie

Jak widać, mimo, że mechanizmy RTTI są w języku już od dawna i “starsi” programiści mieli już okazję się do nich przyzwyczaić, to ich zachowanie nie zawsze jest proste, o czym świadczą chociażby możliwe undefined behavior, które możemy spowodować w naszym programie. W tym artykule nie wspomnieliśmy jednak o wyjątkach, ponieważ uważam, że są one świetnie opisane na blogu Akrzemi.

Mam nadzieję, że udało mi się Wam, czytelnikom przedstawić chociaż jeden fakt na temat RTTI, którego nie znaliście oraz, że artykuł pomoże Wam w używaniu i zrozumieniu zawiłości mechanizmu RTTI.

Bibliografia



Dawid Pilarski

Software Developer w TomTom. Absolwent Politechniki Łódzkiej kierunku Automatyka i Robotyka. Członek komitetu standaryzacyjnego C++, blogger, prywatny nauczyciel C++.

Blog Dawida
Profil na LinkedIn


Podobne wpisy


Pssst! Używamy Cookies. Poprzez używanie naszego serwisu zgadzasz się na odczytywanie i zapisywanie Cookies w swojej przeglądarce.
Polityka Prywatności