Dlaczego warto używać static_cast <int> (x) zamiast (int) x?

667

Słyszałem, że ta static_castfunkcja powinna być preferowana od rzutowania w stylu C lub prostego rzutowania w stylu funkcji. Czy to prawda? Dlaczego?

Tommy Herbert
źródło
36
Sprzeciwiaj się honorze, zapytał i odpowiedział .
Graeme Perrow,
25
Nie zgadzam się, to drugie pytanie dotyczyło opisu różnic między obsadami wprowadzonymi w C ++. To pytanie dotyczy rzeczywistej użyteczności static_cast, która jest nieco inna.
Vincent Robert,
2
Z pewnością moglibyśmy połączyć dwa pytania, ale tym, co musielibyśmy zachować w tym wątku, jest przewaga używania funkcji nad rzutowaniem w stylu C, o czym obecnie wspomina się tylko w odpowiedzi w jednym wierszu w drugim wątku, bez głosów .
Tommy Herbert
6
To pytanie dotyczy typów „wbudowanych”, takich jak int, natomiast pytanie dotyczy typów klas. Wydaje się, że jest to wystarczająco znacząca różnica, aby zasługiwać na osobne wyjaśnienie.
9
static_cast to tak naprawdę operator, a nie funkcja.
ThomasMcLeod

Odpowiedzi:

633

Głównym powodem jest to, że klasyczne odlewy C nie dokonują rozróżnienia między tym, co nazywamy static_cast<>(), reinterpret_cast<>(), const_cast<>(), i dynamic_cast<>(). Te cztery rzeczy są zupełnie inne.

A static_cast<>()jest zwykle bezpieczny. Istnieje poprawna konwersja w języku lub odpowiedni konstruktor, który to umożliwia. Jedynie raz jest to trochę ryzykowne, kiedy rzucisz się na dziedziczną klasę; musisz upewnić się, że obiekt jest w rzeczywistości potomkiem, o którym się twierdzi, że jest zewnętrzny w stosunku do języka (jak flaga w obiekcie). A dynamic_cast<>()jest bezpieczne, dopóki wynik jest sprawdzany (wskaźnik) lub możliwy wyjątek jest uwzględniany (odniesienie).

Z drugiej strony A reinterpret_cast<>()(lub a const_cast<>()) jest zawsze niebezpieczne. Mówisz kompilatorowi: „zaufaj mi: wiem, że to nie wygląda foo(wygląda na to, że nie można go modyfikować), ale tak jest”.

Pierwszym problemem jest to, że prawie niemożliwe jest określenie, który wystąpi w obsadzie w stylu C, bez patrzenia na duże i rozproszone fragmenty kodu i znajomości wszystkich reguł.

Załóżmy, że:

class CDerivedClass : public CMyBase {...};
class CMyOtherStuff {...} ;

CMyBase  *pSomething; // filled somewhere

Teraz te dwie są kompilowane w ten sam sposób:

CDerivedClass *pMyObject;
pMyObject = static_cast<CDerivedClass*>(pSomething); // Safe; as long as we checked

pMyObject = (CDerivedClass*)(pSomething); // Same as static_cast<>
                                     // Safe; as long as we checked
                                     // but harder to read

Zobaczmy jednak prawie identyczny kod:

CMyOtherStuff *pOther;
pOther = static_cast<CMyOtherStuff*>(pSomething); // Compiler error: Can't convert

pOther = (CMyOtherStuff*)(pSomething);            // No compiler error.
                                                  // Same as reinterpret_cast<>
                                                  // and it's wrong!!!

Jak widać, nie ma łatwego sposobu na rozróżnienie tych dwóch sytuacji bez wiedzy o wszystkich zaangażowanych klasach.

Drugi problem polega na tym, że rzutowanie w stylu C jest zbyt trudne do zlokalizowania. W złożonych wyrażeniach może być bardzo trudno zobaczyć rzutki w stylu C. Jest praktycznie niemożliwe napisanie zautomatyzowanego narzędzia, które musi zlokalizować rzutowania w stylu C (na przykład narzędzie wyszukiwania) bez pełnego interfejsu kompilatora C ++. Z drugiej strony łatwo jest wyszukać „static_cast <” lub „reinterpret_cast <”.

pOther = reinterpret_cast<CMyOtherStuff*>(pSomething);
      // No compiler error.
      // but the presence of a reinterpret_cast<> is 
      // like a Siren with Red Flashing Lights in your code.
      // The mere typing of it should cause you to feel VERY uncomfortable.

Oznacza to, że nie tylko rzuty w stylu C są bardziej niebezpieczne, ale znacznie trudniej jest znaleźć je wszystkie, aby upewnić się, że są prawidłowe.

Euro Micelli
źródło
30
Nie należy używać static_castdo odrzucania hierarchii dziedziczenia, ale raczej dynamic_cast. Zwróci to wskaźnik zerowy lub prawidłowy wskaźnik.
David Thornley,
41
@David Thornley: Zwykle się zgadzam. Myślę, że zasygnalizowałem ostrzeżenia, które należy zastosować static_castw tej sytuacji. dynamic_castmoże być bezpieczniejszy, ale nie zawsze jest to najlepsza opcja. Czasami wiesz, że wskaźnik wskazuje na podtyp, nieprzezroczysty dla kompilatora, a a static_castjest szybszy. W co najmniej niektórych środowiskach dynamic_castwymaga opcjonalnej obsługi kompilatora i kosztu działania (włączenie RTTI) i możesz nie chcieć włączać go tylko dla kilku kontroli, które możesz wykonać samodzielnie. RTTI C ++ jest tylko jednym możliwym rozwiązaniem problemu.
Euro Micelli,
18
Twoje twierdzenie o rzutach C jest fałszywe. Wszystkie rzutowania C są konwersjami wartości, z grubsza porównywalnymi do C ++ static_cast. Odpowiednikiem C reinterpret_castjest *(destination_type *)&, tzn. Pobranie adresu obiektu, rzutowanie tego adresu na wskaźnik na inny typ, a następnie dereferencje. Z wyjątkiem przypadku typów znaków lub niektórych typów struktur, dla których C definiuje zachowanie tego konstruktu, generalnie powoduje to niezdefiniowane zachowanie w C.
R .. GitHub STOP HELPING ICE
11
Twoja dokładna odpowiedź dotyczy treści postu. Szukałem odpowiedzi na tytuł „po co używać static_cast <int> (x) zamiast (int) x”. To jest, dla typu int(i inttylko), dlaczego użycie static_cast<int>vs. (int)jako jedyna korzyść wydaje się być ze zmiennymi klasowymi i wskaźnikami. Poproś o rozwinięcie tego.
chux - Przywróć Monikę
31
@chux, for int dynamic_castnie ma zastosowania, ale wszystkie pozostałe powody są ważne . Na przykład: powiedzmy, że vjest parametrem funkcji zadeklarowanym jako float, to (int)vjest static_cast<int>(v). Ale jeśli zmienisz parametr na float*, (int)vpo cichu staje się reinterpret_cast<int>(v)chwilowo static_cast<int>(v)nielegalne i poprawnie przechwytywane przez kompilator.
Euro Micelli,
115

Jedna pragmatyczna wskazówka: możesz łatwo wyszukać słowo kluczowe static_cast w kodzie źródłowym, jeśli planujesz uporządkować projekt.

Karl
źródło
3
możesz wyszukiwać za pomocą nawiasów, np. „(int)”, ale dobra odpowiedź i uzasadniony powód do używania rzutowania w stylu C ++.
Mike
4
@Mike, który znajdzie fałszywe alarmy - deklaracja funkcji z jednym intparametrem.
Nathan Osman
1
Może to dawać fałszywe negatywy: jeśli przeszukujesz bazę kodu, w której nie jesteś jedynym autorem, nie znajdziesz rzutów w stylu C, które inni mogliby wprowadzić z pewnych powodów.
Ruslan
7
Jak zrobienie tego pomogłoby uporządkować projekt?
Bilow
Nie szukałbyś static_cast, ponieważ najprawdopodobniej jest on poprawny. Chcesz odfiltrować static_cast, podczas wyszukiwania reinterpret_cast, const_cast, a może nawet dynamic_cast, ponieważ wskazują one miejsca, które można przeprojektować. C-cast miesza się razem i nie daje ci powodu do rzucania.
Dragan
78

W skrócie :

  1. static_cast<>() daje ci możliwość sprawdzania czasu kompilacji, nie ma obsady w stylu C.
  2. static_cast<>()może być łatwo zauważony w dowolnym miejscu w kodzie źródłowym C ++; przeciwnie, obsada C_Style jest trudniejsza do wykrycia.
  3. Intencje są przekazywane znacznie lepiej przy użyciu rzutowań C ++.

Więcej wyjaśnień :

Obsada statyczna wykonuje konwersje między zgodnymi typami . Jest podobny do obsady w stylu C, ale jest bardziej restrykcyjny. Na przykład rzutowanie w stylu C umożliwia wskaźnikowi całkowitemu wskazywanie znaku.

char c = 10;       // 1 byte
int *p = (int*)&c; // 4 bytes

Ponieważ skutkuje to 4-bajtowym wskaźnikiem wskazującym na 1 bajt przydzielonej pamięci, zapis do tego wskaźnika spowoduje błąd w czasie wykonywania lub nadpisze przylegającą pamięć.

*p = 5; // run-time error: stack corruption

W przeciwieństwie do rzutowania w stylu C, rzutowanie statyczne pozwoli kompilatorowi sprawdzić, czy typy danych wskaźnika i pointee są kompatybilne, co pozwala programiście przechwycić to nieprawidłowe przypisanie wskaźnika podczas kompilacji.

int *q = static_cast<int*>(&c); // compile-time error

Przeczytaj więcej na temat:
Jaka jest różnica między rzutowaniem static_cast <> a rzutowaniem w stylu C
i
rzutowaniem regularnym vs. static_cast vs. dynamic_cast

Rika
źródło
21
Nie zgadzam się, że static_cast<>()jest to bardziej czytelne. Mam na myśli, że czasami tak jest, ale przez większość czasu - szczególnie na podstawowych typach liczb całkowitych - jest po prostu okropnie i niepotrzebnie gadatliwy. Na przykład: Jest to funkcja, która zamienia bajty 32-bitowego słowa. static_cast<uint##>()Czytanie przy użyciu rzutów byłoby prawie niemożliwe , ale dość łatwe do zrozumienia przy użyciu (uint##)rzutów. Zdjęcie kodu: imgur.com/NoHbGve
Todd Lehman
3
@ToddLehman: Dziękuję, ale ja też nie powiedziałem always. (ale w większości przypadków tak) Są pewne przypadki, w których rzutowanie w stylu c jest znacznie bardziej czytelne. To jeden z powodów, dla których casting w stylu c jest wciąż aktywny i działa w imho c ++. :) Nawiasem mówiąc, był to bardzo ładny przykład
Rika
8
Kod @ToddLehman na tym obrazie używa dwóch rzutowań łańcuchowych ( (uint32_t)(uint8_t)), aby osiągnąć, że bajty oprócz najniższych są resetowane. Do tego jest bitowe i ( 0xFF &). Użycie obsad zaciemnia intencję.
Öö Tiib,
28

Pytanie jest większe niż tylko użycie rzutowania static_cast lub rzutowania w stylu C, ponieważ podczas korzystania z rzutów w stylu C zachodzą różne rzeczy. Operatory rzutowania w C ++ mają na celu uwydatnienie tych operacji.

Na powierzchni rzutowania w stylu static_cast i C wyglądają tak samo, na przykład podczas rzutowania jednej wartości na drugą:

int i;
double d = (double)i;                  //C-style cast
double d2 = static_cast<double>( i );  //C++ cast

Oba z nich podają wartość całkowitą na podwójną. Jednak podczas pracy ze wskaźnikami sprawy stają się bardziej skomplikowane. kilka przykładów:

class A {};
class B : public A {};

A* a = new B;
B* b = (B*)a;                                  //(1) what is this supposed to do?

char* c = (char*)new int( 5 );                 //(2) that weird?
char* c1 = static_cast<char*>( new int( 5 ) ); //(3) compile time error

W tym przykładzie (1) być może OK, ponieważ obiekt wskazywany przez A jest tak naprawdę instancją B. Ale co, jeśli nie wiesz w tym momencie kodu, na co właściwie wskazuje? (2) może całkowicie legalny (chcesz spojrzeć tylko na jeden bajt liczby całkowitej), ale może to być również błąd, w którym to przypadku błąd byłby miły, jak (3). Operatory rzutowania w C ++ mają na celu ujawnienie tych problemów w kodzie, zapewniając błędy kompilacji lub wykonania w miarę możliwości.

Zatem do ścisłego „rzutowania wartości” możesz użyć static_cast. Jeśli chcesz odlewania polimorficznego wskaźników w czasie wykonywania, użyj dynamic_cast. Jeśli naprawdę chcesz zapomnieć o typach, możesz użyć reintrepret_cast. Aby po prostu wyrzucić const przez okno, jest const_cast.

Po prostu uwydatniają kod, dzięki czemu wygląda na to, że wiesz, co robiłeś.

Dusty Campbell
źródło
26

static_castoznacza, że ​​nie możesz przypadkowo const_castlub reinterpret_cast, co jest dobrą rzeczą.

DrPizza
źródło
4
Dodatkowe (choć raczej niewielkie) zalety w porównaniu z obsadą w stylu C to to, że wyróżnia się bardziej (robienie czegoś potencjalnie złego powinno wyglądać brzydko) i jest bardziej grep.
Michael Burr,
4
W mojej książce zdolność grep jest zawsze plusem.
Branan
7
  1. Pozwala łatwo znaleźć rzutowania w kodzie za pomocą grep lub podobnych narzędzi.
  2. Wyjaśnia, jaki rodzaj obsady wykonujesz, i angażujesz pomoc kompilatora w egzekwowaniu tego. Jeśli chcesz tylko odrzucić wiązanie, możesz użyć const_cast, co nie pozwoli ci wykonywać innych rodzajów konwersji.
  3. Rzutowania są z natury brzydkie - Ty jako programista unieważniasz, jak kompilator normalnie traktowałby twój kod. Mówisz do kompilatora: „Wiem lepiej od ciebie”. W związku z tym sensowne jest, aby wykonanie obsady było umiarkowanie bolesne i że powinno wystarczyć w kodzie, ponieważ są prawdopodobnym źródłem problemów.

Zobacz Efektywne C ++ Wprowadzenie

JohnMcG
źródło
Całkowicie się z tym zgadzam w przypadku klas, ale czy używanie rzutów w stylu C ++ dla typów POD ma jakiś sens?
Zachary Kraus
Chyba tak. Wszystkie 3 powody dotyczą POD i warto mieć tylko jedną regułę zamiast osobnych dla klas i POD.
JohnMcG
Interesujące, być może będę musiał zmodyfikować sposób, w jaki wykonuję rzutowania w przyszłym kodzie dla typów POD.
Zachary Kraus
7

Chodzi o to, ile bezpieczeństwa typu chcesz narzucić.

Kiedy piszesz (bar) foo(co jest równoważne zreinterpret_cast<bar> foo jeśli nie podałeś operatora konwersji typu), mówisz kompilatorowi, aby zignorował bezpieczeństwo typu, i po prostu rób to, co mu kazano.

Kiedy piszesz static_cast<bar> foo, pytasz kompilatora, aby przynajmniej sprawdził, czy konwersja typu ma sens, a dla typów integralnych wstawił kod konwersji.


EDYCJA 26.02.2014

Napisałem tę odpowiedź ponad 5 lat temu i pomyliłem się. (Patrz komentarze.) Ale wciąż zyskuje poparcie!

Pitarou
źródło
8
(bar) foo nie jest równoważne z reinterpretacją_cast <bar> (foo). Reguły dla „(TYPE) expr” są takie, że wybierze odpowiednią obsadę w stylu C ++, która może obejmować reinterpret_cast.
Richard Corden,
Słuszna uwaga. Euro Micelli udzieliło ostatecznej odpowiedzi na to pytanie.
Pitarou,
1
Poza tym static_cast<bar>(foo)w nawiasach. To samo dotyczy reinterpret_cast<bar>(foo).
LF,
6

Rzutów w stylu C można łatwo przeoczyć w bloku kodu. Rzutowania w stylu C ++ są nie tylko lepszą praktyką; oferują znacznie większą elastyczność.

reinterpret_cast pozwala na konwersję typu całkowego na wskaźnikowy, jednak może być niebezpieczny, jeśli zostanie niewłaściwie użyty.

static_cast oferuje dobrą konwersję typów liczbowych, np. z wyliczeń na ints lub ints na zmiennoprzecinkowe lub dowolne typy danych, których jesteś pewny. Nie wykonuje żadnych kontroli w czasie wykonywania.

Natomiast dynamic_cast wykona te kontrole, oznaczając wszelkie niejednoznaczne przypisania lub konwersje. Działa tylko w przypadku wskaźników i referencji i powoduje obciążenie.

Jest kilka innych, ale są to główne, na które się natkniesz.

Konrad
źródło
4

static_cast, oprócz manipulowania wskaźnikami do klas, może być również używany do wykonywania konwersji wyraźnie zdefiniowanych w klasach, a także do wykonywania standardowych konwersji między podstawowymi typami:

double d = 3.14159265;
int    i = static_cast<int>(d);
prakash
źródło
4
Dlaczego ktoś miałby pisać static_cast<int>(d), kiedy (int)djest o wiele bardziej zwięzły i czytelny? (Mam na myśli w przypadku typów podstawowych, a nie wskaźników obiektowych.)
Todd Lehman
@ gd1 - Dlaczego ktokolwiek miałby stawiać spójność ponad czytelnością? (właściwie pół poważny)
Todd Lehman
2
@ToddLehman: Mnie, biorąc pod uwagę, że tworzenie wyjątków dla niektórych typów tylko dlatego, że są one dla ciebie czymś wyjątkowym, nie ma dla mnie żadnego sensu, a także nie zgadzam się z twoją samą koncepcją czytelności. Krótszy nie oznacza, że ​​jest bardziej czytelny, jak widzę na zdjęciu zamieszczonym w innym komentarzu.
gd1
2
static_cast to wyraźna i świadoma decyzja o dokonaniu bardzo szczególnego rodzaju konwersji. W ten sposób zwiększa jasność intencji. Jest również bardzo przydatny jako znacznik do wyszukiwania plików źródłowych w poszukiwaniu konwersji podczas przeglądu kodu, błędu lub aktualizacji.
Persixty
1
@ToddLehman kontrapunkt: Dlaczego ktoś miałby pisać, (int)dkiedy int{d}jest o wiele bardziej czytelny? Konstruktor, lub podobny do funkcji, jeśli masz (), składnia nie jest tak szybka, aby przekształcić się w koszmarny labirynt nawiasów w złożonych wyrażeniach. W takim przypadku byłoby to int i{d}zamiast int i = (int)d. O wiele lepiej IMO. To powiedziawszy, kiedy potrzebuję tymczasowego wyrażenia, używam static_casti nigdy nie używałem rzutów konstruktorów, nie sądzę. Używam tylko, (C)castsgdy pośpiesznie piszę debugowanie couts ...
podkreślenie_d