25.1 Operator typeid

Argumentem operatora typeid (z nagłówka typeinfo) może być nazwa typu lub dowolne wyrażenie o określonej wartości. Operator zwraca identyfikator typu argumentu, który jest obiektem klasy type_info. Klasa ta, poprzez przeciążenie operatorów ' ==' i ' !=' zapewnia możliwość porównywania obiektów reprezentujących typy, na przykład:

       #include <typeinfo>
       // ...
       double x = 1.5;
       // ...
       if (typeid(x) == typeid(double)) ... // true
       if (typeid(x) == typeid(36.0))   ... // true
       if (typeid(x) == typeid(3))      ... // false
       if (typeid(x) != typeid(3))      ... // true
       if (typeid(x) != typeid(int))    ... // true
Klasa type_info posiada metodę name, która zwraca C-napis zawierający nazwę typu: nazwy te nie muszą pokrywać się ze standardowymi nazwami typów, choć mogą. Na przykład
       typeid(int).name()
zwraca nazwę ' int' w środowisku VC++, ale g++ i Intelowski icpc dają po prostu nazwę ' i'. Zwykle nazwy nie są nam do niczego potrzebne, ważne jest tylko porównywanie typów.

Ciekawsze i bardziej pożyteczne jest rozpoznawanie typów w warunkach dziedziczenia. Szczegóły rozpatrzmy na przykładzie następującego programu:


P200: rtti.cpp     Dynamiczne rozpoznawanie typu

      1.  #include <iostream>
      2.  #include <typeinfo>
      3.  using namespace std;
      4.  
      5.  struct Pojazd { };
      6.  struct Samochod : Pojazd { };
      7.  
      8.  struct Budynek {
      9.      virtual ~Budynek() { }
     10.  };
     11.  struct Stacja : Budynek { };
     12.  
     13.  
     14.  
     15.  int main() {
     16.      Pojazd    poj;
     17.      Samochod  sam1,sam2;
     18.      Samochod* p_sam1 = &sam1;
     19.      Pojazd*   p_sam2 = &sam2;
     20.      Pojazd&   r_sam1 = sam1;
     21.      cout << "    poj: " << typeid(poj).name()     << endl
     22.           << "   sam1: " << typeid(sam1).name()    << endl
     23.           << "   sam2: " << typeid(sam2).name()    << endl
     24.           << " p_sam1: " << typeid(p_sam1).name()  << endl
     25.           << " p_sam2: " << typeid(p_sam2).name()  << endl
     26.           << "*p_sam1: " << typeid(*p_sam1).name() << endl
     27.           << "*p_sam2: " << typeid(*p_sam2).name() << endl
     28.           << " r_sam1: " << typeid(r_sam1).name()  << endl;
     29.  
     30.      cout << "Typy *p_sam1 i *p_sam2 sa "
     31.           << (typeid(*p_sam1) == typeid(*p_sam2) ?
     32.                      "takie same\n" : "rozne\n")   << endl;
     33.  
     34.      Budynek  bud;
     35.      Stacja   sta1,sta2;
     36.      Stacja*  p_sta1 = &sta1;
     37.      Budynek* p_sta2 = &sta2;
     38.      Budynek& r_sta1 = sta1;
     39.      cout << "    bud: " << typeid(bud).name()     << endl
     40.           << "   sta1: " << typeid(sta1).name()    << endl
     41.           << "   sta2: " << typeid(sta2).name()    << endl
     42.           << " p_sta1: " << typeid(p_sta1).name()  << endl
     43.           << " p_sta2: " << typeid(p_sta2).name()  << endl
     44.           << "*p_sta1: " << typeid(*p_sta1).name() << endl
     45.           << "*p_sta2: " << typeid(*p_sta2).name() << endl
     46.           << " r_sta1: " << typeid(r_sta1).name()  << endl;
     47.  
     48.      cout << "Typy *p_sta1 i *p_sta2 sa "
     49.           << (typeid(*p_sta1) == typeid(*p_sta2) ?
     50.                      "takie same\n" : "rozne\n");
     51.  }

Mamy w programie dwie pary klas: klasę Pojazd i dziedziczącą z niej klasę Samochod oraz analogicznie klasę Budynek i dziedziczącą z niej klasę Stacja. Między tymi parami klas zachodzi jednak głęboka różnica: para Pojazd Samochod nie zawiera metod wirtualnych, a więc nie są to klasy polimorficzne. Natomiast para Budynek Stacja jest polimorficzna, bo destruktor klasy Budynek jest wirtualny, a dziedziczenie jest publiczne (nie napisaliśmy tego jawnie, ale użyliśmy słowa kluczowego struct, a nie class). Przypatrzmy się wydrukowi tego programu:
        poj: 6Pojazd                         ( 1)
       sam1: 8Samochod                       ( 2)
       sam2: 8Samochod                       ( 3)
     p_sam1: P8Samochod                      ( 4)
     p_sam2: P6Pojazd                        ( 5)
    *p_sam1: 8Samochod                       ( 6)
    *p_sam2: 6Pojazd                         ( 7)
     r_sam1: 6Pojazd                         ( 8)
    Typy *p_sam1 i *p_sam2 sa rozne          ( 9)

        bud: 7Budynek                        (11)
       sta1: 6Stacja                         (12)
       sta2: 6Stacja                         (13)
     p_sta1: P6Stacja                        (14)
     p_sta2: P7Budynek                       (15)
    *p_sta1: 6Stacja                         (16)
    *p_sta2: 6Stacja                         (17)
     r_sta1: 6Stacja                         (18)
    Typy *p_sta1 i *p_sta2 sa takie same     (19)
Linie 1-3 i 11-13 wydruku nie wymagają komentarza: typ jest dokładnie taki, jaki jest typ obiektu (wiodące znaki są dodawane do nazw typów przez kompilator; nie musimy się nimi przejmować — inny kompilator może wewnętrznie używać innych nazw typów). Ponieważ argumentami operatora typeid są tu obiekty, do których odnosimy się przez ich nazwę, a nie poprzez wskaźnik lub referencję, żadnego polimorfizmu tak czy owak nie ma.

Typem drukowanym w liniach 4-5 (oraz 14-15) wydruku jest typ wskaźnika, a nie wskazywanego przez ten wskaźnik obiektu (nazwa 'P8Samochod' to nazwa typu wskaźnik do 8Samochod; 'P' od pointer). Porównując wydruk z linii 4 i 5 oraz z linii 14 i 15 widzimy, że w tym przypadku polimorfizm też nie ma nic do rzeczy: typem jest prawdziwy, zadeklarowany typ wskaźnika.

Jeśli p jest wskaźnikiem, to wyrażenie typeid(p) określa typ tego wskaźnika, a nie typ obiektu wskazywanego przez ten wskaźnik.

Inaczej jest, kiedy pytamy bezpośrednio o typ obiektu wskazywanego przez wskaźnik, a więc o typ wartości wyrażenia *p, gdzie p jest wskaźnikiem. Ponieważ do obiektu odnosimy się teraz przez wskaźnik, polimorfizm może zadziałać, pod warunkiem, że mamy do czynienia z klasami polimorficznymi, a więc zadeklarowana w nich jest choć jedna metoda wirtualna; może nią być sam destruktor, jak to jest w naszym przykładzie dla pary klas Budynek Stacja.

Spójrzmy na linie 6 i 7 wydruku. Prawdziwym typem obiektów wskazywanych zarówno przez wskaźnik p_sam1 jak i  p_sam2 jest typ pochodny Samochod. Jednak typem wskaźnika w pierwszym przypadku jest Samochod*, a w drugim Pojazd*. Ponieważ te klasy nie są polimorficzne, typ obiektów wskazywanych *p_sam1*p_sam2 zostanie rozpoznany według deklaracji wskaźników, a więc statycznie. Tak więc znalezionym typem wartości wyrażeń *p_sam1*p_sam2 będzie odpowiednio SamochodPojazd. Inaczej jest dla obiektów będących wartościami wyrażeń *p_sta1*p_sta2. Prawdziwy ich typ to typ pochodny Stacja. Odnosimy się do tych obiektów przez wskaźnik, a klasy są polimorficzne. Zatem tym razem rozpoznany zostanie w obu wypadkach prawdziwy typ wskazywanych obiektów — patrz linie 16 i 17.

Rozpoznawanie prawdziwych typów dla klas polimorficznych zachodzi, jak wiemy, również wtedy, gdy do obiektów odnosimy się poprzez referencję. Przykład mamy w liniach 8 i 18: bez polimorfizmu rozpoznany został typ statyczny Pojazd, według zadeklarowanego typu referencji r_sam1, a nie prawdziwy typ obiektu, do którego ta referencja się odnosi, czyli Samochod (linia 8). Natomiast dla klas polimorficznych rozpoznany został prawdziwy typ obiektu (linia 18).

Linie 9 i 19 wskazują jeszcze raz, że porównanie typów obiektów wskazywanych przez wskaźniki lub referencje może zawieść dla klas, które polimorficzne nie są. Typy obiektów *p_sam1*p_sam2 zostały rozpoznane jako różne (linia 9), choć tak naprawdę są takie same, tylko typy wskaźników wskazujących na te obiekty są różne. Dla klas polimorficznych typy *p_sta1*p_sta2 zostały prawidłowo rozpoznane jako takie same (linia 19), mimo że typy wskaźników były różne.

T.R. Werner, 23 lutego 2019; 23:59