10.6 -- Jawna konwersja typów (casting) i static_cast

W lekcji 10.1 -- Niejawna konwersja typów omówiliśmy, że kompilator może użyć niejawnej konwersji typów do konwersji wartości z jednego typu danych na inny. Jeśli chcesz numerycznie promować wartość z jednego typu danych na szerszy typ danych, użycie niejawnej konwersji typów jest w porządku.

Wielu nowych programistów C++ próbuje czegoś takiego:

double d = 10 / 4; // does integer division, initializes d with value 2.0

Ponieważ 10 i 4 oba są typu int, wykonywane jest dzielenie liczb całkowitych, a wyrażenie zwraca się do int wartości 2. Wartość ta jest następnie poddawana konwersji numerycznej na double wartości 2.0 przed użyciem do inicjalizacji zmiennej d. Najprawdopodobniej nie jest to zamierzone.

W przypadku, gdy używasz operandów dosłownych, zastąpienie jednego lub obu literałów całkowitych literałami podwójnymi spowoduje zamiast tego wykonanie podziału zmiennoprzecinkowego:

double d = 10.0 / 4.0; // does floating point division, initializes d with value 2.5

Ale co, jeśli zamiast literałów użyjesz zmiennych? Rozważmy taki przypadek:

int x { 10 };
int y { 4 };
double d = x / y; // does integer division, initializes d with value 2.0

Ponieważ zastosowano tutaj dzielenie przez liczby całkowite, zmienna d otrzyma wartość 2.0. Jak powiedzieć kompilatorowi, że w tym przypadku chcemy użyć dzielenia zmiennoprzecinkowego zamiast dzielenia na liczby całkowite? Dosłownych przyrostków nie można używać ze zmiennymi. Potrzebujemy sposobu na przekonwertowanie jednego (lub obu) operandów zmiennych na typ zmiennoprzecinkowy, tak aby zamiast tego użyte zostało dzielenie zmiennoprzecinkowe.

Na szczęście w C++ dostępnych jest wiele różnych operatorów rzutowania typów (częściej nazywanych rzutowaniem), których programista może użyć, aby kompilator wykonał typ konwersja. Ponieważ rzutowanie jest jawnym żądaniem programisty, ta forma konwersji typu jest często nazywana jawną konwersją typu (w przeciwieństwie do niejawnej konwersji typu, w której kompilator automatycznie wykonuje konwersję typu).

Rzutowanie typów

C++ obsługuje 5 różnych typów rzutowania: static_cast, dynamic_cast, const_cast, reinterpret_cast i rzutowania w stylu C. Pierwsze cztery są czasami określane jako nazwane rzutowania.

Dla zaawansowanych czytelników

CastDescriptionSafe?
static_castWykonuje konwersję typów w czasie kompilacji pomiędzy pokrewnymi typami.Tak
dynamic_castPrzeprowadza konwersje typów w czasie wykonywania na wskaźnikach lub referencjach w hierarchia polimorficzna (dziedziczenie)Tak
const_castDodaje lub usuwa const.Tylko do dodawania const
reinterpret_castReinterpretuje reprezentację jednego typu na poziomie bitowym tak, jakby był to inny typNie
Styl C rzutyWykonuje pewną kombinację static_cast, const_cast lub reinterpret_cast.Nie

Każde użycie działa w ten sam sposób. Jako dane wejściowe rzutowanie przyjmuje wyrażenie (które daje w wyniku wartość lub obiekt) i typ docelowy. Jako dane wyjściowe rzutowanie zwraca wynik konwersji.

Ponieważ są to najczęściej używane rzutowania, w tej lekcji omówimy rzuty w stylu C i static_cast .

Powiązana treść

Omawiamy dynamic_cast w lekcji 25.10 -- Rzutowanie dynamiczne, po omówieniu innych wymaganych tematów.

const_cast i reinterpret_cast należy generalnie unikać, ponieważ są przydatne tylko w rzadkich przypadkach i mogą mogą być szkodliwe, jeśli zostaną użyte nieprawidłowo.

Ostrzeżenie

Unikaj const_cast i reinterpret_cast chyba, że masz bardzo dobry powód, aby ich używać.

w stylu C

W standardowym programowaniu w C rzutowanie odbywa się poprzez operator(), z nazwą typu do konwersji umieszczoną w nawiasach, a wartością do konwersji umieszczoną bezpośrednio po prawej stronie nawiasu zamykającego. W C++ ten typ rzutowania nazywany jest rzutowaniem w stylu w stylu C. Możesz je nadal widzieć w kodzie przekonwertowanym z C.

Na przykład:

#include <iostream>

int main()
{
    int x { 10 };
    int y { 4 };

    std::cout << (double)x / y << '\n'; // C-style cast of x to double

    return 0;
}

W powyższym programie używamy rzutowania w stylu C, aby poinformować kompilator o konieczności konwersji x do double. Ponieważ lewy operand operator/ teraz oblicza wartość zmiennoprzecinkową, prawy operand również zostanie przekonwertowany na wartość zmiennoprzecinkową, a dzielenie zostanie wykonane przy użyciu dzielenia zmiennoprzecinkowego zamiast dzielenia na liczbach całkowitych.

C++ udostępnia również alternatywną formę rzutowania w stylu C, znaną jako rzutowanie w stylu funkcji, które przypomina wywołanie funkcji:

    std::cout << double(x) / y << '\n'; //  // function-style cast of x to double

Rzutowanie w stylu funkcji sprawia, że jest to trochę łatwiej jest określić, co jest konwertowane (ponieważ wygląda to jak standardowy argument funkcji).

Istnieje kilka istotnych powodów, dla których we współczesnym C++ generalnie unika się rzutowania w stylu C.

Po pierwsze, chociaż rzutowanie w stylu C wydaje się być pojedynczym rzutowaniem, w rzeczywistości może wykonywać wiele różnych konwersji w zależności od tego, jak jest używane. Może to obejmować rzutowanie statyczne, rzutowanie stałe lub rzutowanie reinterpretacyjne (z których dwóch ostatnich, o których wspomnieliśmy powyżej, należy unikać). Rzutowanie w stylu C nie wyjaśnia, które rzutowania zostaną faktycznie wykonane, co nie tylko znacznie utrudnia zrozumienie kodu, ale także otwiera drzwi do niezamierzonego niewłaściwego użycia (kiedy myślisz, że wdrażasz proste rzutowanie, a zamiast tego robisz coś niebezpiecznego). Często kończy się to błędem, który zostaje wykryty dopiero w czasie wykonywania.

Ponadto, ponieważ rzutowania w stylu C to tylko nazwa typu, nawias i zmienna lub wartość, są one trudne do zidentyfikowania (co sprawia, że kod jest trudniejszy do odczytania), a jeszcze trudniejsze do wyszukania.

Z drugiej strony, nazwane rzutowania są łatwe do wykrycia i wyszukiwania, wyjaśniają, co robią, mają ograniczone możliwości, a próba spowoduje błąd kompilacji aby je niewłaściwie wykorzystać.

Najlepsza praktyka

Unikaj używania rzutowań w stylu C.

Dla zaawansowanych czytelników

Rzutowanie w stylu C próbuje wykonać następujące rzutowania w C++ w kolejności:

  • const_cast
  • static_cast
  • static_cast, po którym następuje const_cast
  • reinterpret_cast
  • reinterpret_cast, po którym następuje const_cast

Jest jedna rzecz, którą można zrobić za pomocą rzutowania w stylu C, a której nie można zrobić za pomocą rzutowań w C++: Rzuty w stylu C mogą konwertować obiekt pochodny do klasy bazowej, która jest niedostępna (np. ponieważ została odziedziczona prywatnie).

static_cast powinno być używane do rzutowania większości wartości

Zdecydowanie najczęściej używanym rzutowaniem w C++ jest operator rzutowania statycznego , do którego dostęp można uzyskać poprzez static_cast słowo kluczowe. static_cast jest używany, gdy chcemy jawnie przekonwertować wartość jednego typu na wartość innego typu.

Wcześniej widziałeś static_cast konwertowanie a char na int tak, że std::cout wypisuje go jako liczbę całkowitą zamiast znaku:

#include <iostream>

int main()
{
    char c { 'a' };
    std::cout << static_cast<int>(c) << '\n'; // prints 97 rather than a

    return 0;
}

Aby wykonać rzutowanie statyczne, zaczynamy od słowa kluczowego static_cast , a następnie umieszczamy typ do konwersji w środku nawiasy kątowe. Następnie w nawiasie umieszczamy wyrażenie, którego wartość zostanie przeliczona. Zwróć uwagę, jak bardzo składnia wygląda jak wywołanie funkcji o nazwie static_cast<type>() z wyrażeniem, którego wartość zostanie przekonwertowana, podanym jako argument! Statyczne rzutowanie wartości na inny typ wartości zwraca obiekt tymczasowy, który został bezpośrednio zainicjowany przekonwertowaną wartością.

Oto sposób, w jaki byśmy static_cast rozwiązali problem, który przedstawiliśmy we wstępie do tej lekcji:

#include <iostream>

int main()
{
    int x { 10 };
    int y { 4 };

    // static cast x to a double so we get floating point division
    std::cout << static_cast<double>(x) / y << '\n'; // prints 2.5

    return 0;
}

static_cast<double>(x) zwraca obiekt tymczasowy double zawierający przekonwertowaną wartość 10.0. Ten tymczasowy jest następnie używany jako lewy operand dzielenia zmiennoprzecinkowego.

Istnieją dwie ważne właściwości static_cast.

Najpierw static_cast zapewniające sprawdzanie typu w czasie kompilacji. Jeśli spróbujemy przekonwertować wartość na typ, a kompilator nie będzie wiedział, jak przeprowadzić tę konwersję, pojawi się błąd kompilacji.

    // a C-style string literal can't be converted to an int, so the following is an invalid conversion
    int x { static_cast<int>("Hello") }; // invalid: will produce compilation error

Po drugie, static_cast jest (celowo) mniej wydajny niż rzutowanie w stylu C, ponieważ zapobiega pewnym rodzajom niebezpiecznych konwersji (takich jak te, które wymagają reinterpretacji lub odrzucenia const).

Najlepsza praktyka

Zamawiaj static_cast kiedy trzeba przekonwertować wartość z jednego typu na inny.

Dla zaawansowanych czytelników

Ponieważ static_cast używa bezpośredniej inicjalizacji, wszelkie jawne konstruktory typu klasy docelowej będą brane pod uwagę podczas inicjowania obiektu tymczasowego, który ma zostać zwrócony. Konstruktory jawne omawiamy w lekcji 14.16 — Konwersja konstruktorów i jawne słowo kluczowe.

Użycie static_cast aby zawężające konwersje były jawne

Kompilatory często wyświetlają ostrzeżenia, gdy wykonywana jest potencjalnie niebezpieczna (zawężająca) niejawna konwersja typu. Rozważmy na przykład następujący fragment kodu:

int i { 48 };
char ch = i; // implicit narrowing conversion

Rzutowanie int (2 lub 4 bajtów) na char (1 bajt) jest potencjalnie niebezpieczne (ponieważ kompilator nie jest w stanie stwierdzić, czy wartość całkowita przekroczy zakres char , czy nie), dlatego kompilator zazwyczaj wyświetli ostrzeżenie. Gdybyśmy użyli inicjalizacji listy, kompilator zgłosiłby błąd.

Aby obejść ten problem, możemy użyć rzutowania statycznego, aby jawnie przekonwertować naszą liczbę całkowitą na char:

int i { 48 };

// explicit conversion from int to char, so that a char is assigned to variable ch
char ch { static_cast<char>(i) };

Kiedy to robimy, wyraźnie informujemy kompilator, że ta konwersja jest zamierzona i bierzemy na siebie odpowiedzialność za konsekwencje (np. przekroczenie zakresu a char , jeśli tak się stanie). Ponieważ dane wyjściowe tego statycznego rzutowania są typu char, inicjalizacja zmiennej ch nie generuje żadnych niezgodności typów, a tym samym żadnych ostrzeżeń ani błędów.

Oto kolejny przykład, w którym kompilator zazwyczaj narzeka, że konwersja double na int może spowodować utratę danych:

int i { 100 };
i = i / 2.5;

Aby poinformować kompilator, że wyraźnie to chcemy zrób tak:

int i { 100 };
i = static_cast<int>(i / 2.5);

Powiązana treść

Więcej zastosowań static_cast w odniesieniu do typów klas omawiamy na lekcji 14.13 -- Tymczasowe obiekty klas.

Rzutowanie a inicjowanie obiektu tymczasowego

Powiedzmy, że mamy jakąś zmienną x , którą musimy przekonwertować na int. Można to zrobić na dwa konwencjonalne sposoby:

  1. static_cast<int>(x), który zwraca tymczasową int obiektu bezpośrednio-inicjowaną z x.
  2. int { x }, która tworzy tymczasową int obiektu bezpośrednio-zainicjowaną listę z x.

Powinniśmy unikać int ( x ), czyli rzutowania w stylu C. Spowoduje to zwrócenie tymczasowej int bezpośredniej inicjalizacji z wartością x (jak można się spodziewać po składni), ale ma też inne wady wspomniane w sekcji rzutowania w stylu C (takie jak umożliwienie wykonania niebezpiecznej konwersji).

Istnieją (co najmniej) trzy zauważalne różnice pomiędzy static_cast a inicjowaną listą bezpośrednią tymczasowe:

  1. int { x } używa inicjalizacji listy, co uniemożliwia zawężanie konwersji. Jest to świetne rozwiązanie podczas inicjowania zmiennej, ponieważ rzadko kiedy mamy zamiar utracić dane w takich przypadkach. Jednak w przypadku korzystania z rzutowania zakłada się, że wiemy, co robimy, a jeśli chcemy wykonać rzutowanie, które może spowodować utratę części danych, powinniśmy być w stanie to zrobić. Zawężające się ograniczenie konwersji może być w tym przypadku przeszkodą.

Pokażmy tego przykład, łącznie z tym, jak może to prowadzić do problemów specyficznych dla platformy:

#include <iostream>

int main()
{
    int x { 10 };
    int y { 4 };

    // We want to do floating point division, so one of the operands needs to be a floating point type
    std::cout << double{x} / y << '\n'; // okay if int is 32-bit, narrowing if x is 64-bit
}

W tym przykładzie zdecydowaliśmy się na konwersję x do double aby móc dzielić zmiennoprzecinkowe, a nie dzielenie całkowite. W architekturze 32-bitowej będzie to działać dobrze (ponieważ a double może reprezentować wszystkie wartości, które można przechowywać w architekturze 32-bitowej int, więc nie jest to konwersja zawężająca). Jednak w architekturze 64-bitowej tak nie jest, więc konwersja architektury 64-bitowej int do double jest konwersją zawężającą. A ponieważ inicjalizacja listy nie pozwala na zawężanie konwersji, nie zostanie to skompilowane na architekturach, w których int jest 64-bitowa.

  1. static_cast dzięki czemu staje się jaśniejsze, że zamierzamy przeprowadzić konwersję. Chociaż opcja static_cast jest bardziej gadatliwa niż alternatywa inicjowana bezpośrednio z listy, w tym przypadku jest to dobra rzecz, ponieważ ułatwia wykrycie i wyszukanie konwersji. Dzięki temu Twój kod będzie bezpieczniejszy i łatwiejszy do zrozumienia.
  2. Bezpośrednia inicjalizacja listy tymczasowej pozwala tylko na nazwy typu składające się z jednego słowa. Ze względu na dziwne dziwactwo składni, w C++ jest kilka miejsc, w których dozwolone są tylko nazwy typów składające się z jednego słowa (standard C++ nazywa te nazwy „prostymi specyfikatorami typów”). Zatem chociaż int { x } jest prawidłową składnią konwersji, unsigned int { x } nie jest.

Możesz to zobaczyć na własne oczy w poniższym przykładzie, który powoduje błąd kompilacji:

#include <iostream>

int main()
{
    unsigned char c { 'a' };
    std::cout << unsigned int { c } << '\n';

    return 0;
}

Są proste sposoby obejścia tego problemu, najłatwiejszy z nich to użycie aliasu składającego się z jednego słowa:

#include <iostream>

int main()
{
    unsigned char c { 'a' };
    using uint = unsigned int;
    std::cout << uint { c } << '\n';

    return 0;
}

Ale po co zawracać sobie głowę, skoro możesz po prostu static_cast?

Z tych wszystkich powodów: generalnie wolimy static_cast niż bezpośrednią inicjalizację obiektu tymczasowego przez listę.

Najlepsza praktyka

Preferuj static_cast niż inicjowanie obiektu tymczasowego, gdy pożądana jest konwersja.

Czas quizu

Pytanie nr 1

Jaka jest różnica między niejawną i jawną konwersją typu?

Pokaż rozwiązanie

guest
Twój adres e-mail nie zostanie wyświetlony
Znalazłeś błąd? Zostaw komentarz powyżej!
Komentarze związane z poprawkami zostaną usunięte po przetworzeniu, aby pomóc zmniejszyć bałagan. Dziękujemy za pomoc w ulepszaniu witryny dla wszystkich!
Awatary z https://gravatar.com/ są połączone z podanym adresem e-mail.
Powiadamiaj mnie o odpowiedziach:  
247 Komentarze
Najnowsze
Najstarsze Najczęściej głosowane
Wbudowane opinie
Wyświetl wszystkie komentarze