25.2 Operator dynamic_cast

Operator dynamicznego rzutowania (konwersji) stosuje się, gdy prawidłowość przekształcenia nie może być sprawdzona na etapie kompilacji, bo zależy od typu obiektu klasy polimorficznej, który znany jest dopiero w czasie wykonania. Jego użycie ma sens w sytuacjach, gdy mamy do czynienia z obiektami różnych klas powiązanych ze sobą hierarchią dziedziczenia.

Wyobraźmy sobie, że zdefiniowane są klasy  A i dziedzicząca z niej klasa  B. Klasy muszą być polimorficzne, a więc w klasie A musi istnieć choć jedna metoda wirtualna (patrz rozdział o dziedziczeniu ). Załóżmy, że istnieje też funkcja, której parametr ma typ A* lub A&. Czy można wywołać tę funkcję z argumentem typu B* lub B? Ponieważ obiekt klasy B można traktować jako obiekt klasy A, bo zawiera na pewno wszystkie składowe zadeklarowane w klasie A (i, być może, inne), więc takie wywołanie jest legalne i konwersja od typu B* do typu A* zajdzie niejawnie. Na marginesie zauważmy, że mówimy tu o przekazywaniu obiektów do funkcji przez wskaźnik lub referencję: nie przez wartość. Wartości typu  B nie mogą wystąpić w roli wartości typu  A na stosie programu, bo, na przykład, mają inny rozmiar (formalnie jest to możliwe, ale wiąże się z „szatkowaniem” obiektu — na stosie położony zostanie podobiekt klasy  A obiektu klasy  B, co zazwyczaj nie jest tym czego byśmy chcieli).

Inna jest sytuacja, jeśli, odwrotnie, mamy zmienną typu B*, a przypisać jej chcielibyśmy wartość typu A*. Obiekt wskazywany przez ten wskaźnik może, ale nie musi być obiektem klasy pochodnej B. Konwersja zatem w tę stronę, od wskaźnika na obiekt klasy bazowej do wskaźnika na obiekt klasy pochodnej, może się nie powieść. Musi zatem być dokonana jawnie. Ale kompilator nie jest w stanie stwierdzić, czy takie przekształcenie jest prawidłowe, bo nie wie, jaki będzie typ obiektu wskazywanego podczas wykonania programu. Nie można zatem użyć statycznego operatora konwersji. I właśnie wtedy przydaje się operator konwersji dynamicznej dynamic_cast. Składnia jego użycia jest następująca:

       dynamic_cast<Typ>(wyrazenie)
Zachodzą dwa przypadki: Rozpatrzmy przykład:

P198: prgr.cpp     Rzutowanie dynamiczne

      1.  #include <iostream>
      2.  #include <string>
      3.  using namespace std;
      4.  
      5.  class Program {
      6.  protected:
      7.      string name;
      8.  public:
      9.      Program(string n)
     10.          : name(n)
     11.      { }
     12.      virtual void print() = 0;
     13.      virtual ~Program() { };
     14.  };
     15.  
     16.  class Freeware : public Program {
     17.  public:
     18.      Freeware(string n)
     19.          : Program(n)
     20.      { }
     21.      void print() {
     22.          cout << "Free : " << name << endl;
     23.      }
     24.  };
     25.  
     26.  class Shareware : public Program {
     27.      int price;
     28.  public:
     29.      Shareware(string n, int c)
     30.          : Program(n), price(c)
     31.      { }
     32.      void print() {
     33.          cout << "Share: "  << name
     34.               << ", price " << price << endl;
     35.      }
     36.      int getPrice() {
     37.          return price;
     38.      }
     39.  };
     40.  
     41.  int total(Program* prgs[], int size) {
     42.      Shareware* sh;
     43.      int tot = 0;
     44.      for (int i = 0; i < size; ++i) {
     45.          prgs[i]->print();
     46.          if ( sh = dynamic_cast<Shareware*>(prgs[i]) )
     47.              tot += sh->getPrice();
     48.      }
     49.      return tot;
     50.  }
     51.  
     52.  int main() {
     53.      Freeware anjuta("Anjuta"); Shareware wz("WinZip",30);
     54.      Freeware mysql("MySQL");   Shareware rar("RAR",25);
     55.  
     56.      Program* prgs[] = { &anjuta, &wz, &mysql, &rar };
     57.  
     58.      int tot = total(prgs, sizeof(prgs)/sizeof(prgs[0]));
     59.  
     60.      cout << "\nTotal: $" << tot << endl;
     61.  }

Definiujemy w tym programie klasę abstrakcyjną Program i dwie klasy pochodne: FreewareShareware. Klasy są polimorficzne, bo w klasie bazowej zadeklarowana została metoda (czysto) wirtualna print. Zauważmy, że w klasie Shareware zadeklarowaliśmy dodatkowe składowe, których nie było w klasie bazowej. Są nimi pole price i metoda getPrice. Funkcja total pobiera tablicę wskaźników typu Program*. W pętli, dla każdego wskaźnika z tablicy, wywołuje metodę print. Żadne rzutowanie nie jest potrzebne: polimorfizm zadba o to, aby za każdym razem wywołana została metoda z prawdziwej klasy wskazywanego obiektu. Ale funkcja total oprócz wypisania listy programów ma obliczyć całkowity ich koszt. Nie da się jednak wywołać metody getPrice dla wskaźnika typu Program*, bo w klasie Program takiej metody nie ma. Wiemy jednak, że niektóre programy są klasy Shareware i tylko dla nich cena jest w ogóle zdefiniowana. Rzutujemy zatem wskaźnik typu bazowego Program*, który jest typem każdego elementu tablicy, na typ pochodny Shareware* (linia 46). Jeśli to się powiedzie, to zmienna sh typu Shareware* będzie niezerowa i będzie wskazywać na obiekt, który jest na pewno obiektem klasy pochodnej Shareware, a nie klasy Freeware. Zatem teraz będzie możliwe wywołanie dla tego obiektu metody getPrice (linia 47). Jeśli natomiast wskazywany przez kolejny element tablicy obiekt nie jest obiektem typu Shareware (tylko Freeware, bo innej możliwości nie ma), to zmienna sh będzie zerowa, test w instrukcji warunkowej if z linii 46 da wartość false i wywołania funkcji getPrice nie będzie — nie mogłoby się ono powieść, bo w klasie Freeware metody getPrice nie ma.

Wynikiem działania programu jest

    Free : Anjuta
    Share: WinZip, price 30
    Free : MySQL
    Share: RAR, price 25

    Total: $55
Aby zilustrować drugą możliwość, w powyższym programie moglibyśmy zapisać funkcję total następująco:
      1.      int total(Program* prgs[], int size) {
      2.          int tot = 0;
      3.          for (int i = 0; i < size; ++i) {
      4.              prgs[i]->print();
      5.              try {
      6.                  Shareware& sh =
      7.                      dynamic_cast<Shareware&>(*prgs[i]);
      8.                  tot += sh.getPrice();
      9.              } catch(bad_cast) { }
     10.          }
     11.          return tot;
     12.      }
Tym razem rzutujemy (linia 7) nie wskaźnik, ale wartość obiektową wskazywaną przez ten wskaźnik: prgs[i] jest wskaźnikiem, więc *prgs[i] jest l-wartością wskazywanego obiektu. Jeśli rzutowanie do typu Shareware& powiedzie się, to odczytujemy i dodajemy cenę. Jeśli nie powiedzie się, to znaczy obiekt nie był klasy pochodnej Shareware, zgłaszany jest wyjątek i sterowanie nie dochodzi do linii 8, tylko wchodzi do frazy catch, gdzie wyjątek ten jest po prostu ignorowany, po czym program przechodzi do wykonania kolejnego obrotu pętli.

T.R. Werner, 25 lutego 2017; 22:31