Podrozdziały


19.2 Konwersje jawne

W czystym C, tak jak w Javie, możemy zażądać jawnie konwersji od wartości jednego typu do wartości innego typu za pomocą rzutowania. Ma ono postać

       (Typ) wyrazenie
gdzie Typ jest nazwą typu, a  wyrazenie jest wyrażeniem o p-wartości innego typu. Wynikiem jest p-wartość typu Typ, reprezentująca wartość wyrażenia wyrazenie. W Javie tego typu rzutowania są bezpieczne: albo na etapie kompilacji, albo, jeśli to niemożliwe, na etapie wykonania zostanie sprawdzone, czy takie rzutowanie ma sens. W C taka forma rzutowania jest mniej bezpieczna; będzie ono wykonane „siłowo”, czasem zupełnie bezsensownie. Dlatego lepiej jest używać nowych operatorów, wprowadzonych w C++, które wykonują te konwersje w sposób bardziej kontrolowany. Wszystkie one mają postać
       rodzaj_cast<Typ>(wyrazenie)
gdzie zamiast ' rodzaj' należy wstawić static, dynamic, const lub reinterpret. Wynikiem będzie p-wartość typu Typ utworzona na podstawie wartości wyrażenia wyrazenie.


19.2.1 Konwersje uzmienniające

Konwersja uzmienniająca ma postać

       const_cast<Typ>(wyrazenie)
Wyrażenie wyrazenie musi tu być tego samego typu co Typ, tylko z modyfikatorem const lub volatile. A zatem tego rodzaju konwersja usuwa „ustaloność” (lub „ulotność”) i może służyć tylko do tego celu. Z drugiej strony, tego samego efektu nie można uzyskać za pomocą konwersji przy użyciu static_cast, dynamic_cast lub reinterpret_cast: kompilator uznałby taką konwersję const Typ Typ za nielegalną. Rozpatrzmy przykład:


P157: concast.cpp     Usuwanie stałości

      1.  #include <iostream>
      2.  using namespace std;
      3.  
      4.  void changeFirst(char* str, char c) {
      5.      str[0]=c;
      6.  }
      7.  
      8.  int main() {
      9.      const char name[] = "Jenny";
     10.      cout << name << endl;
     11.  
     12.      // name[0]='K';
     13.  
     14.      changeFirst(const_cast<char*>(name),'K');
     15.  
     16.      // changeFirst(name,'K');
     17.  
     18.      cout << name << endl;
     19.  }

Funkcja changeFirst zmienia pierwszą literę przekazanego jej C-napisu. W programie głównym tworzymy ustalony napis name o zawartości "Jenny" (linia 9). Próba jego zmiany w wykomentowanej linii 12 skończyłaby się przerwaniem kompilacji. W linii 14 wysyłamy ten napis do funkcji changeFirst, konwertując argument tak, aby usunąć atrybut stałości. Jak widać z wydruku

    Jenny
    Kenny
po tej operacji zmiana ustalonego napisu powiodła się. Zauważmy też, że bez konwersji, a więc tak jak w wykomentowanej linii 16, wywołać tej funkcji nie byłoby można, bo name jest C-napisem ustalonym, a funkcja changeFirst nie „obiecuje”, poprzez deklarację typu parametru jako const, że napisu przekazanego jako argument nie zmieni (czego zresztą obiecać nie może, bo właśnie ten napis zmienia).

Jeśli deklarujemy zmienne ustalone, to robimy to właśnie po to, aby ich nie można było zmieniać. Zatem użycie konwersji uzmienniającej świadczy o jakiejś niekonsekwencji w programie. Powinno być zatem stosowane tylko w wyjątkowych wypadkach.


19.2.2 Konwersje statyczne

Konwersja statyczna ma postać

       static_cast<Typ>(wyrazenie)
i dokonuje jawnego przekształcenia typu, sprawdzając, czy jest to przekształcenie dopuszczalne. Sprawdzenie odbywa się podczas kompilacji. Często użycie tego operatora jest właściwie zbędne, bo konwersja i tak zostanie dokonana. Jeśli jest to jednak konwersja, w której może wystąpić utrata informacji, to kompilator zwykle ostrzega nas przed jej użyciem. Stosując jawną konwersję statyczną, unikamy tego rodzaju ostrzeżeń kompilatora. Na przykład kompilacja prawidłowego fragmentu kodu
       double x = 4;
       int i = x;
spowoduje wysłanie ostrzeżeń kompilatora
    d.cpp:6: warning: initialization to `int' from `double'
    d.cpp:6: warning: argument to `int' from `double'
których możemy uniknąć jawnie dokonując konwersji:
       double x = 4;
       int i = static_cast<int>(x);
Częstym zastosowaniem rzutowania statycznego jest rzutowanie od typu void* do typu Typ* (konwersja w drugą stronę jest zawsze bezpieczną konwersją standardową, która nie wymaga sprawdzania, więc nie musi być jawna). Takie konwersje stosuje się także do rzutowaia w dół wskaźników typu „wskaźnik do obiektu klasy bazowej” do typu „wskaźnik do obiektu klasy pochodnej” (dla typów niepolimorficznych, o czym powiemy w dalszej części).


19.2.3 Konwersje dynamiczne

Konwersja dynamiczna ma postać

       dynamic_cast<Typ>(wyrazenie)
Konwersje dynamiczne stosuje się, gdy prawidłowość przekształcenia nie może być sprawdzona na etapie kompilacji, bo zależy od typu obiektu klasy polimorficznej. Typ ten jest znany dopiero w czasie wykonania i wtedy ma miejsce sprawdzenie poprawności. Tego rodzaju konwersje używane są wyłącznie w odniesieniu do klas polimorficznych i tylko dla typów wskaźnikowych i referencyjnych. Ponieważ o polimorfizmie jeszcze nie mówiliśmy, pozostawimy dalsze szczegóły do rozdziału o dynamicznym rzutowaniu .


19.2.4 Konwersje wymuszane

„Najsilniejszą” formą konwersji jest konwersja wymuszana. Ma ona postać

       reinterpret_cast<Typ>(wyrazenie)
Użycie takiej konwersji oznacza, że rezygnujemy ze sprawdzania jej poprawności w czasie kompilacji i wykonania i, co za tym idzie, ponosimy pełną odpowiedzialność za jej skutki. Stosuje się ją, gdy wiemy z góry, że ani w czasie kompilacji, ani w czasie wykonania nie będzie możliwe określenie jej sensowności. W ten sposób można, na przykład, dokonać konwersji char* int* lub klasaA* klasaB*, gdzie klasy klasaAklasaB są zupełnie niezależne. Takie konwersje nie są bezpieczne, a ich reaultat może zależeć od używanej platformy czy kompilatora.

Przyjrzyjmy się na przykład poniższemu programowi:


P158: dyncast.cpp     Jawne konwersje wymuszane w C++

      1.  #include <iostream>
      2.  #include <cstring>
      3.  #include <fstream>
      4.  using namespace std;
      5.  
      6.  class Person {
      7.      char nam[30];
      8.      int  age;
      9.  public:
     10.      Person(const char* n, int a) : age(a) {
     11.          strcpy(nam,n);
     12.      }
     13.  
     14.      void info() {
     15.          cout << nam << " (" << age << ")" << endl;
     16.      }
     17.  };
     18.  
     19.  int main() {
     20.      const size_t size = sizeof(Person);
     21.  
     22.      Person john("John Brown",40);
     23.      Person mary("Mary Wiles",26);
     24.  
     25.      ofstream out("person.ob");
     26.      out.write(reinterpret_cast<char*>(&john),size);
     27.      out.write(                (char*) &mary ,size);
     28.      out.close();
     29.  
     30.      char* buff1 = new char[size];
     31.      char* buff2 = new char[size];
     32.      ifstream in("person.ob");
     33.      in.read(buff1,size);
     34.      in.read(buff2,size);
     35.      in.close();
     36.  
     37.      Person* p1 = reinterpret_cast<Person*>(buff1);
     38.      Person* p2 =                 (Person*) buff2 ;
     39.  
     40.      p1->info();
     41.      p2->info();
     42.  
     43.      delete [] buff1;
     44.      delete [] buff2;
     45.  }

Zdefiniowaliśmy tu (linie 22 i 23) dwa obiekty klasy Person, zawierającej jedną składową tablicową i jedną typu int. W liniach 26 i 27 zapisujemy te obiekty w formie binarnej do pliku. Metoda write (patrz rozdział o zapisie nieformatowanym ) ma pierwszy parametr typu const char*. Tymczasem chcemy zapisać reprezentację binarną obiektu john klasy Person: obiekt ten znajduje się pod adresem &john i ma długość sizeof(Person). Typem &john jest Person*, zatem dokonujemy konwersji tego argumentu do typu char* za pomocą rzutowania z użyciem reinterpret_cast. W linii 27 robimy to samo za pomocą rzutowania w stylu C, aby pokazać, te dwie formy mogą tu być użyte zamiennie.

Dwa obiekty klasy Person zapisane na dysk odczytujemy następnie do dwóch tablic znakowych buff1buff2 (linie 33 i 34). Po wczytaniu są to po prostu tablice znaków (bajtów): ani w czasie kompilacji, ani w czasie wykonania system nie ma możliwości sprawdzenia, czy zawarte w nich ciągi bajtów rzeczywiście są reprezentacją obiektów klasy Person. Ale my wiemy, że powinno tak być, bo sami przed chwilą te ciągi bajtów zapisaliśmy. Wymuszamy zatem (linie 37 i 38) konwersję zmiennych buff do typu Person* — znów na dwa sposoby: raz za pomocą reinterpret_cast i raz za pomocą rzutowania w stylu C. Wydruk z linii 40 i 41

    John Brown (40)
    Mary Wiles (26)
przekonuje nas, że konwersja się udała. Pamiętać jednak trzeba, że karkołomne konwersje wymuszane nie zawsze dają rezultaty zgodne z oczekiwaniem. Co gorsza, rezultaty te mogą zależeć od użytego kompilatora i architektury komputera: skoro świadomie zrezygnowaliśmy z kontroli typów, język nie daje nam tu żadnych gwarancji. Tworzona jest wartość, która ma wzorzec bitowy taki jak wartość konwertowana, ale przypisany jest jej inny typ: sensowność tego nie jest ani zapewniana, ani sprawdzana.

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