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) wyrazeniegdzie 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.
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:
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. }
Jenny Kennypo 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.
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).
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 .
„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 klasaA i klasaB 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:
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. }
Dwa obiekty klasy Person zapisane na dysk odczytujemy następnie do dwóch tablic znakowych buff1 i buff2 (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, 21 lutego 2016; 20:17