Rzutowanie typów w C++

Uwaga! Informacje na tej stronie mają ponad 6 lat. Nadal je udostępniam, ale prawdopodobnie nie odzwierciedlają one mojej aktualnej wiedzy ani przekonań.

Wstęp

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:

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:

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:

  1. Microsoft Visual C++ 2005 Professional
  2. g++ 4.0.4

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++.

Wyniki

Oto wyniki próby konwersji między określonymi typami za pomocą określonej metody:

Z short na int

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.

  1. Konwersja niejawna: Prawidłowa konwersja wartości liczbowej.
  2. Konwersja w stylu C: Prawidłowa konwersja wartości liczbowej.
  3. Operator static_cast: Prawidłowa konwersja wartości liczbowej.
  4. Operator 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'
  5. Operator 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.

Z int na short

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.

  1. Konwersja niejawna: Prawidłowa konwersja wartości liczbowej.
  2. Konwersja w stylu C: Prawidłowa konwersja wartości liczbowej.
  3. Operator static_cast: Prawidłowa konwersja wartości liczbowej.
  4. Operator reinterpret_cast: Błąd kompilacji - taki sam jak powyżej.
  5. Operator absolute_cast: Prawidłowa, dosłowna reinterpretacja młodszych bitów liczby.

Z int na unsigned

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.

  1. Konwersja niejawna: Dosłowna reinterpretacja bitów.
  2. Konwersja w stylu C: Dosłowna reinterpretacja bitów.
  3. Operator static_cast: Dosłowna reinterpretacja bitów.
  4. Operator reinterpret_cast: Błąd kompilacji.
  5. Operator absolute_cast: Dosłowna reinterpretacja bitów.

Z unsigned na int

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ą.

  1. Konwersja niejawna: Dosłowna reinterpretacja bitów.
  2. Konwersja w stylu C: Dosłowna reinterpretacja bitów.
  3. Operator static_cast: Dosłowna reinterpretacja bitów.
  4. Operator reinterpret_cast: Błąd kompilacji.
  5. Operator absolute_cast: Dosłowna reinterpretacja bitów.

Z float na double

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.

  1. Konwersja niejawna: Prawidłowa konwersja wartości liczbowej.
  2. Konwersja w stylu C: Prawidłowa konwersja wartości liczbowej.
  3. Operator static_cast: Prawidłowa konwersja wartości liczbowej.
  4. Operator reinterpret_cast: Błąd kompilacji.
  5. Operator 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.

Z double na float

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.

  1. Konwersja niejawna: Prawidłowa konwersja wartości liczbowej. Visual C++ pokazuje ostrzeżenie:
    warning C4244: 'initializing' : conversion from 'SrcT' to 'DstT', possible loss of data
  2. Konwersja w stylu C: Prawidłowa konwersja wartości liczbowej.
  3. Operator static_cast: Prawidłowa konwersja wartości liczbowej.
  4. Operator reinterpret_cast: Błąd kompilacji.
  5. Operator absolute_cast: Poprawna reinterpretacja młodszych bitów, ale wynik nie ma sensu.

Z float na int

To przykładowa konwersja typu zmiennoprzecinkowego na całkowity. Wymaga przeliczenia, a podczas niego obcięta zostaje część ułamkowa wartości.

  1. Konwersja niejawna: Prawidłowa konwersja z obcięciem części ułamkowej. Visual C++ pokazuje takie ostrzeżenie:
    warning C4244: 'initializing' : conversion from 'SrcT' to 'DstT', possible loss of data
    a g++ takie:
    warning: converting to 'main::DstT' from 'main::SrcT'
  2. Konwersja w stylu C: Prawidłowa konwersja z obcięciem części ułamkowej.
  3. Operator static_cast: Prawidłowa konwersja z obcięciem części ułamkowej.
  4. Operator reinterpret_cast: Błąd kompilacji.
  5. Operator absolute_cast: Dosłowna reinterpretacja bitów. Wynik nie ma bezpośredniego sensu.

Z int na float

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.

  1. Konwersja niejawna: Prawidłowa konwersja wartości liczbowej. Visual C++ pokazuje ostrzeżenie:
    warning C4244: 'initializing' : conversion from 'SrcT' to 'DstT', possible loss of data
  2. Konwersja w stylu C: Prawidłowa konwersja wartości liczbowej.
  3. Operator static_cast: Prawidłowa konwersja wartości liczbowej.
  4. Operator reinterpret_cast: Błąd kompilacji.
  5. Operator absolute_cast: Dosłowna reinterpretacja bitów. Wynik nie ma bezpośredniego sensu.

Z void* na int*

To przykład rzutowania ogólniejszego, uniwersalnego wskaźnika na "nie wiadomo co" na wskaźnik do wartości konkretnego typu (obojętnie jakiego).

  1. Konwersja niejawna: Błąd kompilacji. Visual C++ pokazuje taki błąd:
    error C2440: 'initializing' : cannot convert from 'SrcT' to 'DstT'
    a g++ taki:
    error: invalid conversion from 'void*' to 'int*'
  2. Konwersja w stylu C: Prawidłowa reinterpretacja wskaźnika.
  3. Operator static_cast: Prawidłowa reinterpretacja wskaźnika.
  4. Operator reinterpret_cast: Prawidłowa reinterpretacja wskaźnika.
  5. Operator absolute_cast: Prawidłowa reinterpretacja wskaźnika.

Z int* na void*

To rzutowanie wskaźnika na wartość konkretnego typu na ogólny, uniwersalny wskaźnik.

  1. Konwersja niejawna: Prawidłowa reinterpretacja wskaźnika.
  2. Konwersja w stylu C: Prawidłowa reinterpretacja wskaźnika.
  3. Operator static_cast: Prawidłowa reinterpretacja wskaźnika.
  4. Operator reinterpret_cast: Prawidłowa reinterpretacja wskaźnika.
  5. Operator absolute_cast: Prawidłowa reinterpretacja wskaźnika.

Z void* na unsigned

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).

  1. Konwersja niejawna: Błąd kompilacji. Komunikat Visual C++:
    error C2440: 'initializing' : cannot convert from 'SrcT' to 'DstT'
    Komunikat g++:
    error: invalid conversion from 'void*' to 'main::DstT'
  2. Konwersja w stylu C: Dosłowna reinterpretacja bitów. Visual C++ pokazuje ostrzeżenie:
    warning C4311: 'type cast' : pointer truncation from 'SrcT' to 'DstT'
  3. Operator 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'
  4. Operator reinterpret_cast: Dosłowna reinterpretacja bitów. Visual C++ pokazuje ostrzeżenie:
    warning C4311: 'reinterpret_cast' : pointer truncation from 'SrcT' to 'DstT'
  5. Operator absolute_cast: Dosłowna reinterpretacja bitów.

Z unsigned na void*

  1. Konwersja niejawna: Błąd kompilacji. Komunikat Visual C++:
    error C2440: 'initializing' : cannot convert from 'SrcT' to 'DstT'
    Komunikat g++:
    error: invalid conversion from 'void*' to 'main::DstT'
  2. Konwersja w stylu C: Dosłowna reinterpretacja bitów. Visual C++ pokazuje ostrzeżenie:
    warning C4311: 'type cast' : pointer truncation from 'SrcT' to 'DstT'
  3. Operator 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'
  4. Operator reinterpret_cast: Dosłowna reinterpretacja bitów. Visual C++ pokazuje ostrzeżenie:
    warning C4312: 'reinterpret_cast' : conversion from 'SrcT' to 'DstT' of greater size
  5. Operator absolute_cast: Dosłowna reinterpretacja bitów.

Zestawienie

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

absolute_cast

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...

Wnioski

Moim zdaniem z powyższych wyników tego badania wypływają następujące wnioski:

  1. Różne metody konwersji zastosowane dla różnych typów nie zachowują się w sposób jednolity, intuicyjny i oczywisty. Trzeba rozumieć niuanse w ich praktycznym działaniu.
  2. Nie ma praktycznych przesłanek, by rezygnować ze stosowania eleganckiego, zwięzłego rzutowania w stylu C na rzecz rozwlekłych operatorów static_cast i reinterpret_cast.
  3. Brak jest w C++ operatora dosłownej reinterpretacji bitowej między wartościami dowolnych typów. Operator absolute_cast albo sztuczka składniowa, na której jest oparty może uzupełnić ten brak.
Adam Sawicki
17 lipca 2006
[Download] [Dropbox] [pub] [Mirror] [Privacy policy]
Copyright © 2004-2024