Artykuł ten traktuje o praktycznych szczegółach dotyczących działania rzutowania typów w języku programowania C++. Jego poziom zaawansowania oceniam jako średni. Sam artykuł nie jest trudny, ale jego zrozumienie wymaga, aby czytelnik miał już pewne pojęcie o programowaniu w C++ w stopniu przynajmniej podstawowym. Mile widziana jest również wiedza dotycząca reprezentacji bitowej różnego rodzaju wartości liczbowych w pamięci komputera.
Wbrew pozorom omawiany temat nie jest banalny. Mamy w C++ wiele sposobów na konwersje między wartościami róznych typów:
float FloatNumber = IntegerNumber;
float FloatNumber = (float)IntegerNumber;
static_cast
.
float FloatNumber = static_cast<float>(IntegerNumber);
reinterpret_cast
.
int* IntegerPointer = reinterpret_cast<int*>(VoidPointer);
Pokażę też przy okazji wynalazek, który nazywa się
absolute_cast
, a który jest niczym innym, jak zmyślnie napisaną
funkcją mogącą uzupełnić pewne braki C++.
float FloatNumber = absolute_cast<float>(IntegerNumber);
Z kolei efekty rzutowania też bywają różne:
Warto może jeszcze wymienić inne elementy składni języka związane z konwersją czy rzutowaniem typów, o których nie będziemy tutaj mówili:
class ClassB { public: ClassB(const ClassA &AObj);
class ClassA { public: operator ClassB();
const_cast
przeznaczony do znoszenia przydomków
const
i volatile
.
SomeOldFunction(const_cast<char*>(SomeString));
dynamic_cast
do "bezpiecznego" rzutowania w dół
hierarchii klas z wykorzystaniem RTTI.
Marine* marine = dynamic_cast<Marine*>(PointerToGameUnit); if (marine != NULL) ...
Najwyższy czas napisać, co tak naprawdę można będzie znaleźć w tym artykule. Otóż przeprowadziłem pewien eksperyment i to jest właśnie jego wynik. Eksperyment dotyczył zachowania się różnych operatorów i metod konwersji dla różnych typów. Użyłem dwóch kompilatorów:
Kompilowałem z użyciem ustawień domyślnych. Nawet jeśli większość
programistów używa (albo przynajmniej powinna) przełączników takich jak
-Wall
czy -pedantic
, nie o to tutaj chodziło, by
zbadać działanie kompilatorów, tylko samego języka C++.
Oto wyniki próby konwersji między określonymi typami za pomocą określonej metody:
To przykład konwersji z typu całkowitoliczbowego ze znakiem o mniejszej długości na taki sam typ, ale o większej długości. Prawidłowa zamiana wymaga faktycznej konwersji wartości pewnym algorytmem. Nie ma tutaj natomist możliwości utraty precyzji, jakiegoś obicięcia wartości, zawężenia zakresu itp. - konwertujemy na typ o większym zakresie.
static_cast
:
Prawidłowa konwersja wartości liczbowej.
reinterpret_cast
:
Błąd kompilacji. Visual C++ pokazuje coś takiego:
error C2440: 'reinterpret_cast' : cannot convert from 'SrcT' to 'DstT'natomiast g++ coś takiego:
g++: error: invalid cast from type 'main::SrcT' to type 'main::DstT'
absolute_cast
:
Dosłowna reinterpretacja młodszych bitów, ale ponieważ rozmiar wartości
docelowej jest większy, w starszych bitach znajdują się przypadkowe śmieci i
otrzymana wartość jest bzdurna.
To przykład konwersji z typu o większej długości na typ o mniejszej długości. Prawidłowa konwersja wartości liczbowej jest możliwa i wymaga przeliczenia specjalnym algorytmem, ale może przy niej wystąpić błąd (otrzymana wartość będzie nieprawidłowa), jeśli zmienna wejściowa przechowuje liczbę większą, niż da się zapisać w zmiennej docelowej.
static_cast
:
Prawidłowa konwersja wartości liczbowej.
reinterpret_cast
:
Błąd kompilacji - taki sam jak powyżej.
absolute_cast
:
Prawidłowa, dosłowna reinterpretacja młodszych bitów liczby.
To przykład rzutowania wartości z typu liczbowego ze znakiem (zapisanego w
kodzie U2 - najstarszy bit ma wagę ujemną) na liczbę o takiej samej długości,
ale bez znaku (zapisaną w naturalnym kodzie binarnym). Jedyna możliwa konwersja
polega tu na dosłownej reinterpretacji bitów i działa prawidłowo dopóty, dopóki
wartość mieści się w zakresie wspólnym dla obydwu typów. Liczba ujemna typu
int
staje się dla typu unsigned
liczbą dodatnią
większą od 2147483647. Wynika to z różnicy w interpretacji owego najstarszego
bitu.
static_cast
:
Dosłowna reinterpretacja bitów.
reinterpret_cast
:
Błąd kompilacji.
absolute_cast
:
Dosłowna reinterpretacja bitów.
To przykład konwersji z typu całkowitego bez znaku na typ całkowity o takiej samej długości, ale ze znakiem. Odwrotnie niż w poprzednim przykładzie, liczba większa od 2147483647 staje się po reinterpretacji liczbą ujemną.
static_cast
:
Dosłowna reinterpretacja bitów.
reinterpret_cast
:
Błąd kompilacji.
absolute_cast
:
Dosłowna reinterpretacja bitów.
To konwersja między typami zmiennoprzecinkowymi wymagająca przeliczenia. Ponieważ konwertujemy na typ o większym zakresie i precyzji, nie wystepuje niebezpieczeństwo utraty informacji.
static_cast
:
Prawidłowa konwersja wartości liczbowej.
reinterpret_cast
:
Błąd kompilacji.
absolute_cast
:
Bzdurna wartość, ponieważ zmienna wejściowa zostaje uzupełniona o dodatkowe,
wypełnione przypadkowymi danymi bajty, a sama liczba zinterpretowana
dosłownie jako inny typ zmiennoprzecinkowy też nie ma sensu.
To z kolei konwersja z typu zmiennoprzecinkowego o większym zakresie i precyzji na typ zmiennoprzecinkowy o mniejszym zakresie i precyzji. Wymaga przelicznia, a otrzymana wartość może być mniej dokładna albo, w przypadku przekroczenia zakresu, nieprawidłowa.
warning C4244: 'initializing' : conversion from 'SrcT' to 'DstT', possible loss of data
static_cast
:
Prawidłowa konwersja wartości liczbowej.
reinterpret_cast
:
Błąd kompilacji.
absolute_cast
:
Poprawna reinterpretacja młodszych bitów, ale wynik nie ma sensu.
To przykładowa konwersja typu zmiennoprzecinkowego na całkowity. Wymaga przeliczenia, a podczas niego obcięta zostaje część ułamkowa wartości.
warning C4244: 'initializing' : conversion from 'SrcT' to 'DstT', possible loss of dataa g++ takie:
warning: converting to 'main::DstT' from 'main::SrcT'
static_cast
:
Prawidłowa konwersja z obcięciem części ułamkowej.
reinterpret_cast
:
Błąd kompilacji.
absolute_cast
:
Dosłowna reinterpretacja bitów. Wynik nie ma bezpośredniego sensu.
To przykładowa konwersja typu całkowitego na zmiennoprzecinkowy.
Wymaga przeliczenia, a podczas niego może wystąpić utrata dokładności.
Dzieje się tak, ponieważ ta sama liczba bitów (obydwa typy są tej samej
wielkości) musi w przypadku float
przechowywać cechę i mantysę, tak
więc nie jest możliwe pamiętanie pełnego zakresu liczb z typu int
z
dokładnością co do jedynki.
warning C4244: 'initializing' : conversion from 'SrcT' to 'DstT', possible loss of data
static_cast
:
Prawidłowa konwersja wartości liczbowej.
reinterpret_cast
:
Błąd kompilacji.
absolute_cast
:
Dosłowna reinterpretacja bitów. Wynik nie ma bezpośredniego sensu.
To przykład rzutowania ogólniejszego, uniwersalnego wskaźnika na "nie wiadomo co" na wskaźnik do wartości konkretnego typu (obojętnie jakiego).
error C2440: 'initializing' : cannot convert from 'SrcT' to 'DstT'a g++ taki:
error: invalid conversion from 'void*' to 'int*'
static_cast
:
Prawidłowa reinterpretacja wskaźnika.
reinterpret_cast
:
Prawidłowa reinterpretacja wskaźnika.
absolute_cast
:
Prawidłowa reinterpretacja wskaźnika.
To rzutowanie wskaźnika na wartość konkretnego typu na ogólny, uniwersalny wskaźnik.
static_cast
:
Prawidłowa reinterpretacja wskaźnika.
reinterpret_cast
:
Prawidłowa reinterpretacja wskaźnika.
absolute_cast
:
Prawidłowa reinterpretacja wskaźnika.
To konwersja dwóch typów, które z formalnego punktu widzenia są zupełnie różne, a fizycznie są identyczne (wskaźnik to właśnie 32-bitowa liczba całkowita bez znaku).
error C2440: 'initializing' : cannot convert from 'SrcT' to 'DstT'Komunikat g++:
error: invalid conversion from 'void*' to 'main::DstT'
warning C4311: 'type cast' : pointer truncation from 'SrcT' to 'DstT'
static_cast
:
Błąd kompilacji. Komunikat Visual C++:
error C2440: 'static_cast' : cannot convert from 'SrcT' to 'DstT'Komunikat g++:
error: invalid static_cast from type 'void*' to type 'main::DstT'
reinterpret_cast
:
Dosłowna reinterpretacja bitów. Visual C++ pokazuje ostrzeżenie:
warning C4311: 'reinterpret_cast' : pointer truncation from 'SrcT' to 'DstT'
absolute_cast
:
Dosłowna reinterpretacja bitów.
error C2440: 'initializing' : cannot convert from 'SrcT' to 'DstT'Komunikat g++:
error: invalid conversion from 'void*' to 'main::DstT'
warning C4311: 'type cast' : pointer truncation from 'SrcT' to 'DstT'
static_cast
:
Błąd kompilacji. Komunikat Visual C++:
error C2440: 'static_cast' : cannot convert from 'SrcT' to 'DstT'Komunikat g++:
error: invalid static_cast from type 'void*' to type 'main::DstT'
reinterpret_cast
:
Dosłowna reinterpretacja bitów. Visual C++ pokazuje ostrzeżenie:
warning C4312: 'reinterpret_cast' : conversion from 'SrcT' to 'DstT' of greater size
absolute_cast
:
Dosłowna reinterpretacja bitów.
Te same wyniki w postaci tabeli. Gwiazdka oznacza te miejsca, gdzie wystąpiło ostrzeżenie kompilatora.
Z typu | Na typ | Niejawne | W stylu C | static_cast | reinterpret_cast | absolute_cast |
---|---|---|---|---|---|---|
short
| int
| Konwersja | Konwersja | Konwersja | BŁĄD | Reinterpretacja |
int
| short
| Konwersja | Konwersja | Konwersja | BŁĄD | Reinterpretacja |
int
| unsigned
| Reinterpretacja | Reinterpretacja | Reinterpretacja | BŁĄD | Reinterpretacja |
unsigned
| int
| Reinterpretacja | Reinterpretacja | Reinterpretacja | BŁĄD | Reinterpretacja |
float
| double
| Konwersja | Konwersja | Konwersja | BŁĄD | Reinterpretacja |
double
| float
| Konwersja * | Konwersja | Konwersja | BŁĄD | Reinterpretacja |
float
| int
| Konwersja * | Konwersja | Konwersja | BŁĄD | Reinterpretacja |
int
| float
| Konwersja * | Konwersja | Konwersja | BŁĄD | Reinterpretacja |
void*
| int*
| BŁĄD | Reinterpretacja | Reinterpretacja | Reinterpretacja | Reinterpretacja |
int*
| void*
| Reinterpretacja | Reinterpretacja | Reinterpretacja | Reinterpretacja | Reinterpretacja |
void*
| unsigned
| BŁĄD | Reinterpretacja * | BŁĄD | Reinterpretacja * | Reinterpretacja |
unsigned
| void*
| BŁĄD | Reinterpretacja * | BŁĄD | Reinterpretacja * | Reinterpretacja |
Operator absolute_cast
to tak naprawdę zmyślnie napisany szablon
funkcji. Jego wynalazcą jest Karol Kuczmarski "Xion". Oto jej kod:
template <typename destT, typename srcT> destT & absolute_cast(srcT &v) { return reinterpret_cast<destT&>(v); } template <typename destT, typename srcT> const destT & absolute_cast(const srcT &v) { return reinterpret_cast<const destT&>(v); }
Dzięki takiej budowie i automatycznej dedukcji drugiego parametru można go używać identycznie jak wbudowanych operatorów konwersji. Jak on działa? Jest to tak naprawdę eleganckie obudowanie znanej w C++ sztuczki, która pozwala na rzutowanie przez dosłowną reinterpretację wartości między dowolnymi typami. Polega ona na pobraniu adresu wartości, zrzutowaniu go na wskaźnik do innego typu i wyłuskaniu z powrotem wartości spod tego wskaźnika, już typu docelowego.
float f = 10.5f; unsigned i = * (int*) & f; cout << i << endl;
To samo można otrzymać prościej poprzez zrzutowanie samej wartości na referencję do innego typu.
float f = 10.5f; unsigned i = (int&) f; cout << i << endl;
Po co robić takie rzutowania?
Pozornie mogłoby się to wydawać bez sensu, ale czasami bywa przydatne.
Doskonałym przykładem jest generator liczb losowych wynaleziony przez
Mikaela. Dysponując funkcją RandUint
generującą liczby
pseudolosowe typu unsigned
o rozkładzie równomiernym z pełnego zakresu 0 .. 0xFFFFFFFF i znając
budowę bitową liczby typu float
możemy otrzymać bardzo prostą i
szybką funkcję generującą losowe liczby zmiennoprzecinkowe z zakresu 0.0f ..
1.0f:
float RandFloat() { return absolute_cast<float>( RandUint() & 0x007FFFFF | 0x3F800000 ) - 1.0f; }
Oczywiście każda taka sztuczka jest zależna od, jak to się określa, "implementacji", czyli nie będzie przenośna na inne platformy sprzętowe. Jednak ostatecznie, czy to aż takie ważne? W końcu nie każdy pisze aż tak przenosne programy, by działały na jakiś egzotycznych maszynach, na których liczby mają inne rozmiary albo bajty są ułożone odwrotnie, a na 32-bitowych pecetach jest zawsze tak samo niezależnie, czy to w Windowsie, czy w Linuksie...
Moim zdaniem z powyższych wyników tego badania wypływają następujące wnioski:
static_cast
i reinterpret_cast
.
absolute_cast
albo sztuczka
składniowa, na której jest oparty może uzupełnić ten brak.