Menu

Rzutowanie typów w C++: static_cast i konwersje niejawne

Jak działa rzutowanie typów w C++: konwersje niejawne, pułapka dzielenia całkowitego i cztery nazwane rzutowania (static_cast, const_cast, reinterpret_cast, dynamic_cast) wraz z pułapkami, które po cichu gubią dane.

Na tej stronie są działające edytory: edytuj, uruchamiaj i od razu zobacz wynik.

Czym jest rzutowanie typów

Rzutowanie typów (type casting) to konwersja wartości z jednego typu na inny: zamiana double na int, char na jego kod liczbowy albo wskaźnika na klasę bazową na wskaźnik na klasę pochodną. Część tych konwersji C++ wykonuje automatycznie, ale właśnie w tych cichych kryją się błędy.

Są dwa rodzaje: konwersje niejawne, które dzieją się same, oraz jawne rzutowania, które zapisujesz w kodzie. Jeden z objawów znasz już ze strony o operatorach: dzielenie całkowite. Rzutowanie pozwala przejąć nad nim kontrolę.

Konwersje niejawne

Gdy mieszasz w wyrażeniu różne typy liczbowe, C++ promuje "mniejszy" typ do "większego", żeby obie strony pasowały. Zwykle robi to, czego oczekujesz.

Kłopoty zaczynają się, gdy konwersja idzie w przeciwną stronę: z szerszego typu do węższego. To konwersja zawężająca (narrowing) i może po cichu gubić dane.

Zamiana liczby zmiennoprzecinkowej na całkowitą obcina w stronę zera: nie zaokrągla, więc 3.99 staje się 3. A wciśnięcie 300 do char powoduje przepełnienie. Wiele kompilatorów tu ostrzega, niektóre nie. Gdy naprawdę chcesz zawęzić wartość, zapisz to jawnie rzutowaniem, żeby kolejna osoba czytająca kod wiedziała, że to celowe.

Jak ominąć pułapkę dzielenia całkowitego

Najczęstszy powód rzutowania to dzielenie. Gdy oba operandy są całkowite, / wykonuje dzielenie całkowite i wyrzuca resztę.

Rozwiązanie to static_cast<double> na jednym operandzie przed dzieleniem. Częsty błąd to static_cast<double>(got / total): to za późno, bo w chwili rzutowania got / total ma już wartość 0, więc dostajesz 0.0. Rzutuj operand, nie wynik.

static_cast: rzutowanie domyślne

C++ oferuje cztery nazwane rzutowania. Tego, którego użyjesz w 95% przypadków, zapisuje się jako static_cast<T>(value): wykonuje ono dobrze zdefiniowane konwersje między powiązanymi typami, czyli konwersje liczbowe, enum na int, void* z powrotem na wskaźnik z typem oraz przejścia w górę i w dół hierarchii klas, gdy znasz już typ.

Wybieraj static_cast zamiast starego rzutowania w stylu C (int)balance. Rzutowanie w stylu C spróbuje dowolnej konwersji, byle kod się skompilował, łącznie z niebezpiecznymi opisanymi niżej, więc może po cichu usunąć const albo zinterpretować surowe bajty na nowo. static_cast pozwala tylko na konwersje, które kompilator potrafi uzasadnić, a rozwlekły zapis static_cast<...> łatwo wyszukać podczas code review.

// Unikaj: rzutowanie w stylu C, bez siatki bezpieczeństwa:
int dollars = (int) balance;

// Wybieraj: jawne, sprawdzane, łatwe do wyszukania:
int dollars = static_cast<int>(balance);

Pozostałe trzy rzutowania (używaj oszczędnie)

Pozostałe rzutowania istnieją do konkretnych, wąskich zadań. Sięgaj po nie tylko wtedy, gdy static_cast naprawdę nie wystarczy.

const_cast usuwa (lub dodaje) const. Jego jedyne uzasadnione zastosowanie to wywołanie API w stylu C, które zapomniało oznaczyć parametr jako const. Modyfikowanie przez const_cast obiektu, który pierwotnie zadeklarowano jako const, to niezdefiniowane zachowanie.

void legacyApi(char* msg);   // stare API, nie przyjmuje const

const char* text = "hello";
legacyApi(const_cast<char*>(text));   // w porządku tylko wtedy, gdy legacyApi do niego nie zapisuje

reinterpret_cast interpretuje na nowo surowy wzorzec bitów, na przykład wskaźnik jako całkowity adres. Nie wykonuje żadnej konwersji i jest skrajnie niebezpieczne: prawie zawsze to sygnał, że warto przemyśleć projekt.

dynamic_cast bezpiecznie konwertuje wskaźnik lub referencję do klasy bazowej w dół, do typu pochodnego, w czasie działania programu, na podstawie rzeczywistego typu obiektu. Wymaga polimorficznej klasy bazowej (klasy z co najmniej jedną funkcją wirtualną) i zwraca nullptr, gdy rzutowanie się nie udaje.

Gdyby a wskazywało na jakieś inne Animal, dynamic_cast<Dog*> zwróciłoby nullptr i wykonałaby się gałąź else. Właśnie dlatego jest to bezpieczniejsze niż ślepe użycie static_cast do schodzenia w dół hierarchii.

Częste błędy

  • Rzutowanie wyniku zamiast operandu. static_cast<double>(a / b) najpierw gubi część ułamkową. Rzutuj a albo b.
  • Założenie, że zamiana na int zaokrągla. Ona obcina: static_cast<int>(2.99) to 2. Do zaokrąglania służą std::round, std::lround i podobne.
  • Sięganie po rzutowanie w stylu C. Ukrywa ono, jaka konwersja zachodzi. Z static_cast dostaniesz błąd kompilacji przy niebezpiecznej konwersji zamiast cichej niespodzianki.
  • Zawężanie do zbyt małego typu. Rzutowanie 300 na char albo ogromnego long na int powoduje zawinięcie lub przepełnienie. Wybierz typ docelowy wystarczająco szeroki dla zakresu wartości.

Dalej: if-else

Skoro potrafisz już czysto konwertować i porównywać wartości, następny krok to podejmowanie na ich podstawie decyzji. Instrukcja if-else wykonuje różny kod w zależności od tego, czy warunek jest true: to fundament każdego programu z rozgałęzieniami.

Najczęściej zadawane pytania

Jaka jest różnica między static_cast a rzutowaniem w stylu C w C++?

Rzutowanie w stylu C, takie jak (int)x, próbuje po kolei każdej konwersji: może po cichu zamienić się w niebezpieczne reinterpret_cast albo usunąć const. static_cast<int>(x) wykonuje tylko powiązane konwersje, które kompilator potrafi zweryfikować, więc bezsensowne rzutowania są odrzucane. We współczesnym C++ zawsze wybieraj static_cast zamiast rzutowań w stylu C: jest bezpieczniejsze i dużo łatwiej je wyszukać w kodzie.

Jak rzutować int na double w C++?

Użyj static_cast<double>(x). Najważniejsze jest to przy dzieleniu: 5 / 2 to dzielenie całkowite i daje 2, ale static_cast<double>(5) / 2 daje 2.5. Rzutuj jeden operand przed wykonaniem dzielenia: rzutowanie wyniku, static_cast<double>(5 / 2), jest spóźnione i nadal daje 2.0.

Dlaczego rzutowanie dużej wartości na mniejszy typ daje w C++ złą liczbę?

Konwersja do typu, który nie pomieści wartości, to konwersja zawężająca. Przy zamianie liczby zmiennoprzecinkowej na całkowitą część ułamkowa zostaje obcięta (static_cast<int>(3.99) to 3), a liczba całkowita spoza zakresu albo się zawija (unsigned), albo daje wynik zależny od implementacji (signed). Kompilator zwykle cię nie powstrzyma, więc rzutuj świadomie i upewnij się, że typ docelowy jest wystarczająco szeroki.

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ