Kiedy należy używać static_cast, dynamic_cast, const_cast i reinterpret_cast?

2490

Jakie są właściwe zastosowania:

  • static_cast
  • dynamic_cast
  • const_cast
  • reinterpret_cast
  • Obsada w stylu C. (type)value
  • Obsada w stylu funkcjonalnym type(value)

Jak decyduje się, którego użyć w jakich konkretnych przypadkach?

e.James
źródło
3
Aby zapoznać się z niektórymi przydatnymi konkretnymi przykładami użycia różnych rodzajów rzutów, możesz sprawdzić pierwszą odpowiedź na podobne pytanie w tym innym temacie .
TeaMonkie
2
Możesz znaleźć naprawdę dobre odpowiedzi na swoje pytanie powyżej. Chciałbym jednak dodać jeszcze jeden punkt, @ e.James „Nie ma nic, co mogliby zrobić nowi operatorzy rzutowania c ++, a rzutowanie w stylu c nie. Są one dodawane mniej więcej dla lepszej czytelności kodu”.
BreakBadSP
@BreakBadSP Nowe castingi służą nie tylko lepszej czytelności kodu. Są po to, aby utrudniać robienie niebezpiecznych rzeczy, takich jak odrzucanie const lub rzucanie wskaźników zamiast ich wartości. static_cast ma znacznie mniej możliwości zrobienia czegoś niebezpiecznego niż obsada w stylu ac!
FourtyTwo
@FourtyTwo zgodził się
BreakBadSP

Odpowiedzi:

2569

static_castto pierwsza obsada, której powinieneś spróbować użyć. Robi takie rzeczy, jak niejawna konwersja między typami (np. intDo floatlub wskaźnik do void*), a także może wywoływać jawne funkcje konwersji (lub niejawne). W wielu przypadkach wyraźne stwierdzenie static_castnie jest konieczne, ale należy zauważyć, że T(something)składnia jest równoważna (T)somethingi należy jej unikać (więcej na ten temat później). A T(something, something_else)jest jednak bezpieczne i gwarantuje wywołanie konstruktora.

static_castmoże również rzutować poprzez hierarchie dziedziczenia. Nie jest konieczne podczas rzucania w górę (w kierunku klasy podstawowej), ale podczas rzucania w dół można go używać, dopóki nie przejdzie w wyniku virtualdziedziczenia. Nie sprawdza jednak i jest niezdefiniowanym zachowaniem static_castsprowadzającym hierarchię do typu, który w rzeczywistości nie jest typem obiektu.


const_castmoże być użyty do usunięcia lub dodania constdo zmiennej; żadna inna obsada C ++ nie jest w stanie go usunąć (nawet reinterpret_cast). Należy zauważyć, że modyfikowanie poprzedniej constwartości jest niezdefiniowane tylko wtedy, gdy zmienna oryginalna to const; jeśli użyjesz go do constzdjęcia odniesienia do czegoś, co nie zostało zadeklarowane const, jest to bezpieczne. Może to być przydatne na przykład przy przeciążaniu funkcji składowych na podstawie const. Można go również użyć do dodania constdo obiektu, na przykład do wywołania przeciążenia funkcji elementu.

const_castdziała również podobnie volatile, choć jest to mniej powszechne.


dynamic_castsłuży wyłącznie do radzenia sobie z polimorfizmem. Możesz rzutować wskaźnik lub odwołanie do dowolnego typu polimorficznego na dowolny inny typ klasy (typ polimorficzny ma co najmniej jedną funkcję wirtualną, zadeklarowaną lub odziedziczoną). Możesz go używać do czegoś więcej niż tylko rzucania w dół - możesz rzucać na boki, a nawet na inny łańcuch. dynamic_castBędzie odszukać żądany obiekt i zwraca go, jeśli to możliwe. Jeśli nie może, wróci nullptrw przypadku wskaźnika lub wrzuci std::bad_castw przypadku odniesienia.

dynamic_castma jednak pewne ograniczenia. Nie działa, jeśli w hierarchii dziedziczenia znajduje się wiele obiektów tego samego typu (tak zwany „przerażający diament”) i nie korzystasz z virtualdziedziczenia. Może również przechodzić tylko przez dziedziczenie publiczne - zawsze nie uda mu się przejść protectedani privateodziedziczyć. Rzadko jest to jednak problem, ponieważ takie formy dziedziczenia są rzadkie.


reinterpret_castjest najbardziej niebezpieczną obsadą i powinna być używana bardzo oszczędnie. Przekształca jeden typ bezpośrednio w inny - na przykład rzutuje wartość z jednego wskaźnika na inny, lub przechowuje wskaźnik w jednym intlub wielu innych paskudnych rzeczach. W dużej mierze jedyną gwarancją, którą otrzymujesz, reinterpret_castjest to, że normalnie, jeśli rzutujesz wynik z powrotem na oryginalny typ, otrzymasz dokładnie tę samą wartość (ale nie, jeśli typ pośredni jest mniejszy niż typ oryginalny). Istnieje również szereg konwersji, reinterpret_castktórych nie można wykonać. Jest używany przede wszystkim do szczególnie dziwnych konwersji i manipulacji bitami, takich jak przekształcanie surowego strumienia danych w rzeczywiste dane lub przechowywanie danych w małych bitach wskaźnika w wyrównanych danych.


C-style cast i funkcja stylu odlewane są odlewy z użyciem (type)objectlub type(object), odpowiednio, i są funkcjonalnie równoważne. Są one zdefiniowane jako pierwsza z następujących czynności:

  • const_cast
  • static_cast (ignorując ograniczenia dostępu)
  • static_cast (patrz wyżej) const_cast
  • reinterpret_cast
  • reinterpret_cast, następnie const_cast

Dlatego może być używany jako zamiennik innych rzutów w niektórych przypadkach, ale może być wyjątkowo niebezpieczny ze względu na możliwość przekształcenia się w rzut reinterpret_cast, a ten ostatni powinien być preferowany, gdy potrzebne jest jawne rzucanie, chyba że masz pewność, static_castże powiedzie się lub reinterpret_castzakończy się niepowodzeniem . Nawet wtedy rozważ dłuższą, bardziej wyraźną opcję.

Rzutowania w stylu C również ignorują kontrolę dostępu podczas wykonywania static_castrzutu, co oznacza, że ​​mogą wykonać operację, jakiej nie może wykonać żaden inny rzut. Jest to jednak głównie kludge, a moim zdaniem jest to kolejny powód do unikania rzutów w stylu C.

coppro
źródło
17
Dynamic_cast jest tylko dla typów polimorficznych. musisz go używać tylko podczas rzucania do klasy pochodnej. static_cast jest z pewnością pierwszą opcją, chyba że potrzebujesz dynamicznej funkcji dynamic_cast. Ogólnie rzecz biorąc, nie jest to jakaś cudowna srebrna kula „sprawdzająca typ”.
czerwiec
2
Świetna odpowiedź! Jedna szybka uwaga: static_cast może być konieczne, aby rzucić hierarchię w przypadku, gdy masz pochodną * i rzucić w Base * &, ponieważ podwójne wskaźniki / referencje nie automagicznie rzutują hierarchii. Taką (szczerze mówiąc, nieczęstą) sytuację spotkałem dwie minuty temu. ;-)
bartgol
5
* „żadna inna obsada C ++ nie jest w stanie usunąć const(nawet reinterpret_cast)”… naprawdę? Co reinterpret_cast<int *>(reinterpret_cast<uintptr_t>(static_cast<int const *>(0)))?
user541686,
29
Myślę, że ważnym brakującym szczegółem powyżej jest to, że dynamic_cast ma ujemny wpływ na wydajność w czasie wykonywania w porównaniu do static lub reinterpret_cast. Jest to ważne, np. W oprogramowaniu czasu rzeczywistego.
jfritz42
5
Warto wspomnieć, że reinterpret_castczęsto jest to broń z wyboru w przypadku zestawów nieprzezroczystych typów danych API
Class Skeleton
333

Służy dynamic_castdo konwertowania wskaźników / referencji w hierarchii dziedziczenia.

Użyj static_castdo konwersji zwykłego typu.

Służy reinterpret_castdo reinterpretacji wzorów bitowych na niskim poziomie. Używaj z dużą ostrożnością.

Użyj const_castdo rzucania const/volatile. Unikaj tego, chyba że utkniesz przy użyciu niepoprawnego interfejsu API.

Fred Larson
źródło
2
Uważaj z dynamic_cast. Opiera się na RTTI i nie będzie działać zgodnie z oczekiwaniami między granicami bibliotek współdzielonych. Po prostu dlatego, że budujesz bibliotekę wykonywalną i współdzieloną niezależnie, nie ma znormalizowanego sposobu synchronizacji RTTI między różnymi kompilacjami. Z tego powodu w bibliotece Qt istnieje qobject_cast <>, który używa informacji o typie QObject do sprawdzania typów.
user3150128,
198

(Wiele wyjaśnień teoretycznych i koncepcyjnych podano powyżej)

Poniżej kilka praktycznych przykładów, gdy użyłem static_cast , dynamic_cast , const_cast , reinterpret_cast .

(Odnosi się to również do zrozumienia wyjaśnienia: http://www.cplusplus.com/doc/tutorial/typecasting/ )

static_cast:

OnEventData(void* pData)

{
  ......

  //  pData is a void* pData, 

  //  EventData is a structure e.g. 
  //  typedef struct _EventData {
  //  std::string id;
  //  std:: string remote_id;
  //  } EventData;

  // On Some Situation a void pointer *pData
  // has been static_casted as 
  // EventData* pointer 

  EventData *evtdata = static_cast<EventData*>(pData);
  .....
}

dynamic_cast:

void DebugLog::OnMessage(Message *msg)
{
    static DebugMsgData *debug;
    static XYZMsgData *xyz;

    if(debug = dynamic_cast<DebugMsgData*>(msg->pdata)){
        // debug message
    }
    else if(xyz = dynamic_cast<XYZMsgData*>(msg->pdata)){
        // xyz message
    }
    else/* if( ... )*/{
        // ...
    }
}

const_cast:

// *Passwd declared as a const

const unsigned char *Passwd


// on some situation it require to remove its constness

const_cast<unsigned char*>(Passwd)

reinterpret_cast:

typedef unsigned short uint16;

// Read Bytes returns that 2 bytes got read. 

bool ByteBuffer::ReadUInt16(uint16& val) {
  return ReadBytes(reinterpret_cast<char*>(&val), 2);
}
Sumit Arora
źródło
31
Teoria niektórych innych odpowiedzi jest dobra, ale wciąż myląca, widząc te przykłady po przeczytaniu innych odpowiedzi naprawdę sprawia, że ​​wszystkie mają sens. To bez przykładów, wciąż nie byłem pewien, ale dzięki nim jestem teraz pewien, co oznaczają inne odpowiedzi.
Solx
1
O ostatnim użyciu reinterpret_cast: czy to nie to samo, co używanie static_cast<char*>(&val)?
Lorenzo Belli
3
@LorenzoBelli Oczywiście, że nie. Próbowałeś? Ten ostatni nie jest poprawnym C ++ i blokuje kompilację. static_castdziała tylko między typami z określonymi konwersjami, widoczną relacją według dziedziczenia lub do / z void *. Na wszystko inne są inne obsady. reinterpret castna dowolny char *typ zezwala się na odczyt reprezentacji dowolnego obiektu - i jest to jeden z niewielu przypadków, w których to słowo kluczowe jest przydatne, a nie szalony generator implementacji / niezdefiniowane zachowanie. Ale nie jest to uważane za „normalną” konwersję, więc nie jest dozwolone (zwykle) bardzo konserwatywne static_cast.
underscore_d
2
reinterpret_cast jest dość powszechny, gdy pracujesz z oprogramowaniem systemowym, takim jak bazy danych. W większości przypadków piszesz własny menedżer stron, który nie ma pojęcia o typie danych przechowywanych na stronie i zwraca po prostu pusty wskaźnik. Do wyższych poziomów należy dokonać reinterpretacji obsady i wywnioskowanie tego, jak chcą.
Sohaib
1
Przykład const_cast wykazuje niezdefiniowane zachowanie. Zmiennej zadeklarowanej jako const nie można de-const-ed. Jednak zmienna zadeklarowana jako non-const, która jest przekazywana do funkcji przyjmującej odwołanie do const, może w tej funkcji zostać de-const -red bez jej UB.
Johann Gerell
99

Może to pomóc, jeśli znasz trochę elementów wewnętrznych ...

static_cast

  • Kompilator C ++ już wie, jak konwertować typy skalerów, takie jak float na int. Użyj static_castdla nich.
  • Gdy poprosisz kompilator o konwersję z typu Ana B, konstruktor static_castwywołań Bprzechodzi Ajako param. Alternatywnie Amoże mieć operator konwersji (tj A::operator B().). Jeśli Bnie ma takiego konstruktora lub Anie ma operatora konwersji, pojawia się błąd czasu kompilacji.
  • Przesyłanie z A*do B*zawsze kończy się powodzeniem, jeśli A i B są w hierarchii dziedziczenia (lub nieważne), w przeciwnym razie wystąpi błąd kompilacji.
  • Gotcha : Jeśli rzutujesz wskaźnik bazowy na wskaźnik pochodny, ale jeśli rzeczywisty obiekt nie jest tak naprawdę pochodnym typem, nie pojawi się błąd. Otrzymujesz zły wskaźnik i najprawdopodobniej segfault w czasie wykonywania. To samo dotyczy A&się B&.
  • Gotcha : Cast z Derived do Base lub viceversa tworzy nową kopię! Dla osób pochodzących z C # / Java może to być ogromna niespodzianka, ponieważ wynik jest w zasadzie odciętym obiektem utworzonym z Derived.

dynamic_cast

  • dynamic_cast używa informacji o typie środowiska wykonawczego, aby ustalić, czy rzutowanie jest prawidłowe. Na przykład, (Base*)aby (Derived*)może zakończyć się niepowodzeniem, jeśli wskaźnik nie jest w rzeczywistości typu pochodnej.
  • Oznacza to, że dynamic_cast jest bardzo drogi w porównaniu do static_cast!
  • Na A*celu B*, jeśli obsada jest nieprawidłowy następnie dynamic_cast powróci nullptr.
  • Dla A&aby B&jeśli obsada jest nieprawidłowy następnie dynamic_cast rzuci bad_cast wyjątek.
  • W przeciwieństwie do innych rzutów, istnieje narzut związany z czasem działania.

const_cast

  • Podczas gdy static_cast może robić non-const, ale nie może być odwrotnie. Const_cast może działać na dwa sposoby.
  • Jednym z przykładów, w których się to przydaje, jest iteracja przez jakiś kontener, set<T>który zwraca tylko jego elementy jako const, aby upewnić się, że nie zmienisz jego klucza. Jeśli jednak masz zamiar zmodyfikować niekluczowe elementy obiektu, powinno być w porządku. Możesz użyć const_cast, aby usunąć constness.
  • Innym przykładem jest, gdy chcesz wdrożyć, T& SomeClass::foo()a także const T& SomeClass::foo() const. Aby uniknąć powielania kodu, możesz zastosować const_cast, aby zwrócić wartość jednej funkcji z drugiej.

reinterpret_cast

  • To w zasadzie mówi, że bierz te bajty w tym miejscu pamięci i traktuj to jako dany obiekt.
  • Na przykład możesz załadować 4 bajty liczby zmiennoprzecinkowej do 4 bajtów liczby całkowitej, aby zobaczyć, jak wyglądają bity w liczbach zmiennoprzecinkowych.
  • Oczywiście, jeśli dane nie są poprawne dla tego typu, możesz dostać segfault.
  • Dla tej obsady nie ma narzutów związanych z czasem wykonywania.
Shital Shah
źródło
Dodałem informacje o operatorze konwersji, ale jest też kilka innych rzeczy, które należy naprawić i nie czuję się zbyt komfortowo aktualizując to za bardzo. Przedmioty to: 1. If you cast base pointer to derived pointer but if actual object is not really derived type then you don't get error. You get bad pointer and segfault at runtime.Dostajesz UB, co może skutkować awarią w czasie wykonywania, jeśli masz szczęście. 2. Odlewy dynamiczne można również stosować w odlewach krzyżowych. 3. Rzutowanie Const może w niektórych przypadkach powodować UB. Użycie mutablemoże być lepszym wyborem do wdrożenia logicznej stałości.
Adrian
1
@Adrian masz rację pod każdym względem. Odpowiedź jest napisana dla ludzi mniej więcej na poziomie początkującym i nie chciałem ich przytłaczać wszystkimi innymi komplikacjami, które przychodzą mutable, castingiem krzyżowym itp.
Shital Shah,
16

Czy to odpowiada na twoje pytanie?

Nigdy nie korzystałem reinterpret_casti zastanawiam się, czy trafienie na skrzynkę, która tego potrzebuje, nie jest zapachem złego designu. W bazie kodu, nad którą pracuję, dynamic_castużywa się dużo. Różnica static_castpolega na tym, że dynamic_castsprawdza środowisko uruchomieniowe , które może (bezpieczniej) lub nie (więcej narzutu) być tym, czego chcesz (patrz msdn ).

andreas buykx
źródło
3
Użyłem reintrepret_cast w jednym celu - wydobycie bitów z podwójnego (taki sam rozmiar jak długi na mojej platformie).
Joshua
2
reinterpret_cast jest potrzebny np. do pracy z obiektami COM. CoCreateInstance () ma parametr wyjściowy typu void ** (ostatni parametr), w którym przekażesz wskaźnik zadeklarowany jako np. „INetFwPolicy2 * pNetFwPolicy2”. Aby to zrobić, musisz napisać coś takiego jak reinterpret_cast <void **> (& pNetFwPolicy2).
Serge Rogatch
1
Być może istnieje inne podejście, ale używam reinterpret_castdo wydobywania fragmentów danych z tablicy. Na przykład, jeśli mam char*duży bufor pełen spakowanych danych binarnych, które muszę przejść i uzyskać pojedyncze prymitywy różnych typów. Coś w tym stylu:template<class ValType> unsigned int readValFromAddress(char* addr, ValType& val) { /*On platforms other than x86(_64) this could do unaligned reads, which could be bad*/ val = (*(reinterpret_cast<ValType*>(addr))); return sizeof(ValType); }
James Matta
Nigdy nie korzystałem reinterpret_cast, nie ma zbyt wielu zastosowań.
Pika the Wizard of the Whales
Osobiście widziałem reinterpret_castużywane tylko z jednego powodu. Widziałem dane surowego obiektu przechowywane w typie danych „obiektu blob” w bazie danych, a następnie, gdy dane są pobierane z bazy danych, reinterpret_castsą używane do przekształcenia tych surowych danych w obiekt.
ImaginaryHuman072889
15

Oprócz innych dotychczasowych odpowiedzi, tutaj jest nieoczywisty przykład, w którym static_castnie jest wystarczający, więc reinterpret_castjest potrzebny. Załóżmy, że istnieje funkcja, która w parametrze wyjściowym zwraca wskaźniki do obiektów różnych klas (które nie dzielą wspólnej klasy podstawowej). Prawdziwym przykładem takiej funkcji jest CoCreateInstance()(zobacz ostatni parametr, który w rzeczywistości jest void**). Załóżmy, że żądasz określonej klasy obiektu od tej funkcji, więc z góry znasz typ wskaźnika (co często robisz dla obiektów COM). W tym przypadku nie można rzutować wskaźnik do wskaźnika do swojej void**ze static_castmusisz reinterpret_cast<void**>(&yourPointer).

W kodzie:

#include <windows.h>
#include <netfw.h>
.....
INetFwPolicy2* pNetFwPolicy2 = nullptr;
HRESULT hr = CoCreateInstance(__uuidof(NetFwPolicy2), nullptr,
    CLSCTX_INPROC_SERVER, __uuidof(INetFwPolicy2),
    //static_cast<void**>(&pNetFwPolicy2) would give a compile error
    reinterpret_cast<void**>(&pNetFwPolicy2) );

Jednak static_castdziała na prostych wskaźników (nie wskaźniki do wskaźników), więc powyższy kod może zostać przepisany w celu uniknięcia reinterpret_cast(w cenie dodatkową zmienną) w następujący sposób:

#include <windows.h>
#include <netfw.h>
.....
INetFwPolicy2* pNetFwPolicy2 = nullptr;
void* tmp = nullptr;
HRESULT hr = CoCreateInstance(__uuidof(NetFwPolicy2), nullptr,
    CLSCTX_INPROC_SERVER, __uuidof(INetFwPolicy2),
    &tmp );
pNetFwPolicy2 = static_cast<INetFwPolicy2*>(tmp);
Serge Rogatch
źródło
Czy nie działa coś takiego &static_cast<void*>(pNetFwPolicy2)zamiast static_cast<void**>(&pNetFwPolicy2)?
jp48,
9

Podczas gdy inne odpowiedzi ładnie opisywały wszystkie różnice między rzutami w C ++, chciałbym dodać krótką notatkę, dlaczego nie powinieneś używać rzutów w stylu C (Type) vari Type(var).

Dla początkujących w C ++ rzutowania w stylu C wyglądają jak nadzbiór nad rzutowaniami C ++ (static_cast <> (), dynamic_cast <> (), const_cast <> (), reinterpret_cast <> ()) i ktoś może je preferować nad rzutami C ++ . W rzeczywistości obsada w stylu C jest nadzbiorem i jest krótsza do napisania.

Głównym problemem w obsadach w stylu C jest to, że ukrywają prawdziwą intencję twórców obsady. Rzutowania w stylu C mogą wykonywać praktycznie wszystkie typy rzutowania, od normalnie bezpiecznych rzutów wykonywanych przez static_cast <> () i dynamic_cast <> () do potencjalnie niebezpiecznych rzutowań, takich jak const_cast <> (), gdzie można zmienić modyfikator const, aby zmienne const można modyfikować i reinterpretować <cast (>), które mogą nawet ponownie interpretować wartości całkowite na wskaźniki.

Oto próbka.

int a=rand(); // Random number.

int* pa1=reinterpret_cast<int*>(a); // OK. Here developer clearly expressed he wanted to do this potentially dangerous operation.

int* pa2=static_cast<int*>(a); // Compiler error.
int* pa3=dynamic_cast<int*>(a); // Compiler error.

int* pa4=(int*) a; // OK. C-style cast can do such cast. The question is if it was intentional or developer just did some typo.

*pa4=5; // Program crashes.

Głównym powodem, dla którego dodano obsady C ++ do tego języka, było umożliwienie programistom wyjaśnienia swoich zamiarów - dlaczego zamierza to zrobić. Używając rzutowań w stylu C, które są w pełni poprawne w C ++, czynisz swój kod mniej czytelnym i bardziej podatnym na błędy, szczególnie dla innych programistów, którzy nie stworzyli twojego kodu. Aby twój kod był bardziej czytelny i wyraźny, zawsze powinieneś preferować rzutowania w C ++ niż rzutowania w stylu C.

Oto krótki cytat z książki Bjarne Stroustrup (autor C ++) The C ++ Programming Language 4. wydanie - strona 302.

Ta rzutowanie w stylu C jest znacznie bardziej niebezpieczne niż nazwane operatory konwersji, ponieważ notacja jest trudniejsza do zauważenia w dużym programie, a rodzaj konwersji zamierzony przez programistę nie jest jednoznaczny.

Timmy_A
źródło
5

Aby to zrozumieć, rozważmy poniższy fragment kodu:

struct Foo{};
struct Bar{};

int main(int argc, char** argv)
{
    Foo* f = new Foo;

    Bar* b1 = f;                              // (1)
    Bar* b2 = static_cast<Bar*>(f);           // (2)
    Bar* b3 = dynamic_cast<Bar*>(f);          // (3)
    Bar* b4 = reinterpret_cast<Bar*>(f);      // (4)
    Bar* b5 = const_cast<Bar*>(f);            // (5)

    return 0;
}

Tylko linia (4) kompiluje się bez błędów. Tylko reinterpret_cast można użyć do konwersji wskaźnika na obiekt na wskaźnik na dowolny niepowiązany typ obiektu.

Należy zauważyć, że: dynamic_cast nie powiedzie się w czasie wykonywania, jednak w większości kompilatorów również nie uda się go skompilować, ponieważ w strukturze rzutowanego wskaźnika nie ma żadnych funkcji wirtualnych, co oznacza, że dynamic_cast będzie działał tylko ze wskaźnikami klasy polimorficznej .

Kiedy używać obsady C ++ :

  • Użyj static_cast jako odpowiednika rzutowania w stylu C, który dokonuje konwersji wartości, lub gdy musimy jawnie podwyższyć wskaźnik z klasy do jej nadklasy.
  • Użyj const_cast, aby usunąć kwalifikator const.
  • Użyj reinterpret_cast, aby wykonać niebezpieczne konwersje typów wskaźników na liczby całkowite i inne oraz na inne typy wskaźników. Używaj tego tylko wtedy, gdy wiemy, co robimy i rozumiemy problemy z aliasingiem.
Pankaj Kumar Thapa
źródło
2

static_castwidok kontra dynamic_castkontra reinterpret_castwewnętrzne na downcast / upcast

W tej odpowiedzi chcę porównać te trzy mechanizmy na konkretnym przykładzie upcast / downcast i przeanalizować, co dzieje się z podstawowymi wskaźnikami / pamięcią / zestawem, aby uzyskać konkretne zrozumienie ich porównania.

Wierzę, że da to dobrą intuicję na temat różnic między tymi obsadami:

  • static_cast: robi jedno przesunięcie adresu w czasie wykonywania (mały wpływ w czasie działania) i nie sprawdza bezpieczeństwa, czy downcast jest poprawny.

  • dyanamic_cast: robi to samo przesunięcie adresu jak w czasie wykonywania static_cast, ale także i kosztowną kontrolę bezpieczeństwa, czy downcast jest poprawny przy użyciu RTTI.

    Ta kontrola bezpieczeństwa pozwala zapytać, czy wskaźnik klasy bazowej jest określonego typu w czasie wykonywania, sprawdzając, czy zwrot nullptrwskazuje nieprawidłowy downcast.

    Dlatego jeśli twój kod nie jest w stanie tego sprawdzić nullptri podjąć prawidłowej akcji nie przerywania, powinieneś użyć static_castzamiast dynamicznego rzutowania.

    Jeśli przerwanie jest jedyną czynnością, jaką może wykonać Twój kod, być może chcesz tylko włączyć dynamic_castkompilacje debugowania ( -NDEBUG) i użyć static_castinaczej, np. Tak jak tutaj , aby nie spowalniać szybkiego uruchamiania.

  • reinterpret_cast: nic nie robi w czasie wykonywania, nawet przesunięcie adresu. Wskaźnik musi wskazywać dokładnie odpowiedni typ, nawet klasa podstawowa nie działa. Zasadniczo nie chcesz tego, chyba że zaangażowane są strumienie surowych bajtów.

Rozważ następujący przykład kodu:

main.cpp

#include <iostream>

struct B1 {
    B1(int int_in_b1) : int_in_b1(int_in_b1) {}
    virtual ~B1() {}
    void f0() {}
    virtual int f1() { return 1; }
    int int_in_b1;
};

struct B2 {
    B2(int int_in_b2) : int_in_b2(int_in_b2) {}
    virtual ~B2() {}
    virtual int f2() { return 2; }
    int int_in_b2;
};

struct D : public B1, public B2 {
    D(int int_in_b1, int int_in_b2, int int_in_d)
        : B1(int_in_b1), B2(int_in_b2), int_in_d(int_in_d) {}
    void d() {}
    int f2() { return 3; }
    int int_in_d;
};

int main() {
    B2 *b2s[2];
    B2 b2{11};
    D *dp;
    D d{1, 2, 3};

    // The memory layout must support the virtual method call use case.
    b2s[0] = &b2;
    // An upcast is an implicit static_cast<>().
    b2s[1] = &d;
    std::cout << "&d           " << &d           << std::endl;
    std::cout << "b2s[0]       " << b2s[0]       << std::endl;
    std::cout << "b2s[1]       " << b2s[1]       << std::endl;
    std::cout << "b2s[0]->f2() " << b2s[0]->f2() << std::endl;
    std::cout << "b2s[1]->f2() " << b2s[1]->f2() << std::endl;

    // Now for some downcasts.

    // Cannot be done implicitly
    // error: invalid conversion from ‘B2*’ to ‘D*’ [-fpermissive]
    // dp = (b2s[0]);

    // Undefined behaviour to an unrelated memory address because this is a B2, not D.
    dp = static_cast<D*>(b2s[0]);
    std::cout << "static_cast<D*>(b2s[0])            " << dp           << std::endl;
    std::cout << "static_cast<D*>(b2s[0])->int_in_d  " << dp->int_in_d << std::endl;

    // OK
    dp = static_cast<D*>(b2s[1]);
    std::cout << "static_cast<D*>(b2s[1])            " << dp           << std::endl;
    std::cout << "static_cast<D*>(b2s[1])->int_in_d  " << dp->int_in_d << std::endl;

    // Segfault because dp is nullptr.
    dp = dynamic_cast<D*>(b2s[0]);
    std::cout << "dynamic_cast<D*>(b2s[0])           " << dp           << std::endl;
    //std::cout << "dynamic_cast<D*>(b2s[0])->int_in_d " << dp->int_in_d << std::endl;

    // OK
    dp = dynamic_cast<D*>(b2s[1]);
    std::cout << "dynamic_cast<D*>(b2s[1])           " << dp           << std::endl;
    std::cout << "dynamic_cast<D*>(b2s[1])->int_in_d " << dp->int_in_d << std::endl;

    // Undefined behaviour to an unrelated memory address because this
    // did not calculate the offset to get from B2* to D*.
    dp = reinterpret_cast<D*>(b2s[1]);
    std::cout << "reinterpret_cast<D*>(b2s[1])           " << dp           << std::endl;
    std::cout << "reinterpret_cast<D*>(b2s[1])->int_in_d " << dp->int_in_d << std::endl;
}

Kompiluj, uruchamiaj i dezasembluj za pomocą:

g++ -ggdb3 -O0 -std=c++11 -Wall -Wextra -pedantic -o main.out main.cpp
setarch `uname -m` -R ./main.out
gdb -batch -ex "disassemble/rs main" main.out

gdzie setarchjest używany do wyłączenia ASLR aby ułatwić porównanie przebiegów.

Możliwe wyjście:

&d           0x7fffffffc930
b2s[0]       0x7fffffffc920
b2s[1]       0x7fffffffc940
b2s[0]->f2() 2
b2s[1]->f2() 3
static_cast<D*>(b2s[0])            0x7fffffffc910
static_cast<D*>(b2s[0])->int_in_d  1
static_cast<D*>(b2s[1])            0x7fffffffc930
static_cast<D*>(b2s[1])->int_in_d  3
dynamic_cast<D*>(b2s[0])           0
dynamic_cast<D*>(b2s[1])           0x7fffffffc930
dynamic_cast<D*>(b2s[1])->int_in_d 3
reinterpret_cast<D*>(b2s[1])           0x7fffffffc940
reinterpret_cast<D*>(b2s[1])->int_in_d 32767

Teraz, jak wspomniano na stronie : https://en.wikipedia.org/wiki/Virtual_method_table, aby skutecznie wspierać wirtualne wywołania metod, struktura danych pamięci Dmusi wyglądać mniej więcej tak:

B1:
  +0: pointer to virtual method table of B1
  +4: value of int_in_b1

B2:
  +0: pointer to virtual method table of B2
  +4: value of int_in_b2

D:
  +0: pointer to virtual method table of D (for B1)
  +4: value of int_in_b1
  +8: pointer to virtual method table of D (for B2)
 +12: value of int_in_b2
 +16: value of int_in_d

Kluczowym faktem jest to, że struktura danych pamięci Dzawiera w sobie strukturę pamięci zgodną ze strukturą wewnętrzną B1i B2wewnętrzną.

Dlatego dochodzimy do krytycznego wniosku:

Upcast lub downcast musi jedynie przesunąć wartość wskaźnika o wartość znaną w czasie kompilacji

W ten sposób, gdy Dzostanie przekazany do tablicy typów bazowych, rzutowany typ faktycznie oblicza to przesunięcie i wskazuje coś, co wygląda dokładnie tak, jak prawidłowe B2w pamięci:

b2s[1] = &d;

poza tym, że ten ma Dzamiast tego vtable B2, dlatego wszystkie wirtualne połączenia działają transparentnie.

Teraz możemy wreszcie wrócić do odlewania czcionek i analizy naszego konkretnego przykładu.

Z wyjścia standardowego widzimy:

&d           0x7fffffffc930
b2s[1]       0x7fffffffc940

Dlatego domyślnie static_castwykonane tam poprawnie obliczyło przesunięcie od pełnej Dstruktury danych w 0x7fffffffc930 do B2podobnej, która jest w 0x7fffffffc940. Wnioskujemy również, że między 0x7fffffffc930 a 0x7fffffffc940 prawdopodobnie są to B1dane i vtable.

Następnie w sekcjach spuszczanych łatwo jest zrozumieć, dlaczego zawiodły nieprawidłowe i dlaczego:

  • static_cast<D*>(b2s[0]) 0x7fffffffc910: kompilator właśnie poszedł w górę 0x10 w bajtach czasu kompilacji, aby spróbować przejść od a B2do zawierającegoD

    Ale ponieważ b2s[0]nie było D, wskazuje teraz na nieokreślony region pamięci.

    Demontaż to:

    49          dp = static_cast<D*>(b2s[0]);
       0x0000000000000fc8 <+414>:   48 8b 45 d0     mov    -0x30(%rbp),%rax
       0x0000000000000fcc <+418>:   48 85 c0        test   %rax,%rax
       0x0000000000000fcf <+421>:   74 0a   je     0xfdb <main()+433>
       0x0000000000000fd1 <+423>:   48 8b 45 d0     mov    -0x30(%rbp),%rax
       0x0000000000000fd5 <+427>:   48 83 e8 10     sub    $0x10,%rax
       0x0000000000000fd9 <+431>:   eb 05   jmp    0xfe0 <main()+438>
       0x0000000000000fdb <+433>:   b8 00 00 00 00  mov    $0x0,%eax
       0x0000000000000fe0 <+438>:   48 89 45 98     mov    %rax,-0x68(%rbp)

    więc widzimy, że GCC:

    • sprawdź, czy wskaźnik ma wartość NULL, a jeśli tak, zwróć wartość NULL
    • w przeciwnym razie odejmij od niego 0x10, aby osiągnąć to, Dco nie istnieje
  • dynamic_cast<D*>(b2s[0]) 0: C ++ faktycznie stwierdził, że rzutowanie było nieprawidłowe i zwrócił nullptr!

    Nie można tego zrobić w czasie kompilacji, a my potwierdzimy to z dezasemblacji:

    59          dp = dynamic_cast<D*>(b2s[0]);
       0x00000000000010ec <+706>:   48 8b 45 d0     mov    -0x30(%rbp),%rax
       0x00000000000010f0 <+710>:   48 85 c0        test   %rax,%rax
       0x00000000000010f3 <+713>:   74 1d   je     0x1112 <main()+744>
       0x00000000000010f5 <+715>:   b9 10 00 00 00  mov    $0x10,%ecx
       0x00000000000010fa <+720>:   48 8d 15 f7 0b 20 00    lea    0x200bf7(%rip),%rdx        # 0x201cf8 <_ZTI1D>
       0x0000000000001101 <+727>:   48 8d 35 28 0c 20 00    lea    0x200c28(%rip),%rsi        # 0x201d30 <_ZTI2B2>
       0x0000000000001108 <+734>:   48 89 c7        mov    %rax,%rdi
       0x000000000000110b <+737>:   e8 c0 fb ff ff  callq  0xcd0 <__dynamic_cast@plt>
       0x0000000000001110 <+742>:   eb 05   jmp    0x1117 <main()+749>
       0x0000000000001112 <+744>:   b8 00 00 00 00  mov    $0x0,%eax
       0x0000000000001117 <+749>:   48 89 45 98     mov    %rax,-0x68(%rbp)

    Najpierw jest sprawdzanie NULL i zwraca NULL, jeśli einput ma wartość NULL.

    W przeciwnym razie ustawia niektóre argumenty w RDX, RSI i RDI oraz wywołania __dynamic_cast.

    Nie mam teraz cierpliwości, aby dalej to analizować, ale jak powiedzieli inni, jedynym sposobem na to jest __dynamic_castdostęp do niektórych dodatkowych struktur danych w pamięci RTTI, które reprezentują hierarchię klas.

    Dlatego musi zaczynać od B2wpisu dla tej tabeli, a następnie przejść tę hierarchię klas, aż stwierdzi, że vtable dla Dtypecast z b2s[0].

    Dlatego reinterpretacja obsady jest potencjalnie droga! Oto przykład, w którym łatka jednowarstwowa przekształcająca dynamic_casta static_castw złożony projekt skróciła czas działania o 33%! .

  • reinterpret_cast<D*>(b2s[1]) 0x7fffffffc940ten po prostu wierzy nam na ślepo: powiedzieliśmy, że jest Dadres b2s[1], a kompilator nie wykonuje obliczeń przesunięcia.

    Ale to źle, ponieważ D faktycznie ma 0x7fffffffc930, a co 0x7fffffffc940 to struktura podobna do B2 wewnątrz D! Więc dostęp do śmieci.

    Możemy to potwierdzić z przerażającego -O0zestawu, który po prostu przesuwa wartość:

    70          dp = reinterpret_cast<D*>(b2s[1]);
       0x00000000000011fa <+976>:   48 8b 45 d8     mov    -0x28(%rbp),%rax
       0x00000000000011fe <+980>:   48 89 45 98     mov    %rax,-0x68(%rbp)

Powiązane pytania:

Testowane na Ubuntu 18.04 amd64, GCC 7.4.0.

Ciro Santilli
źródło