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?
Jakie są właściwe zastosowania:
static_cast
dynamic_cast
const_cast
reinterpret_cast
(type)value
type(value)
Jak decyduje się, którego użyć w jakich konkretnych przypadkach?
Odpowiedzi:
static_cast
to pierwsza obsada, której powinieneś spróbować użyć. Robi takie rzeczy, jak niejawna konwersja między typami (np. int
Do float
lub wskaźnik do void*
), a także może wywoływać jawne funkcje konwersji (lub niejawne). W wielu przypadkach wyraźne stwierdzenie static_cast
nie jest konieczne, ale należy zauważyć, że T(something)
składnia jest równoważna (T)something
i 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_cast
moż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 virtual
dziedziczenia. Nie sprawdza jednak i jest niezdefiniowanym zachowaniem static_cast
sprowadzającym hierarchię do typu, który w rzeczywistości nie jest typem obiektu.
const_cast
może być użyty do usunięcia lub dodania const
do zmiennej; żadna inna obsada C ++ nie jest w stanie go usunąć (nawet reinterpret_cast
). Należy zauważyć, że modyfikowanie poprzedniej const
wartości jest niezdefiniowane tylko wtedy, gdy zmienna oryginalna to const
; jeśli użyjesz go do const
zdję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 const
do obiektu, na przykład do wywołania przeciążenia funkcji elementu.
const_cast
działa również podobnie volatile
, choć jest to mniej powszechne.
dynamic_cast
sł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_cast
Będzie odszukać żądany obiekt i zwraca go, jeśli to możliwe. Jeśli nie może, wróci nullptr
w przypadku wskaźnika lub wrzuci std::bad_cast
w przypadku odniesienia.
dynamic_cast
ma 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 virtual
dziedziczenia. Może również przechodzić tylko przez dziedziczenie publiczne - zawsze nie uda mu się przejść protected
ani private
odziedziczyć. Rzadko jest to jednak problem, ponieważ takie formy dziedziczenia są rzadkie.
reinterpret_cast
jest 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 int
lub wielu innych paskudnych rzeczach. W dużej mierze jedyną gwarancją, którą otrzymujesz, reinterpret_cast
jest 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_cast
któ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)object
lub 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_cast
zakoń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_cast
rzutu, 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.
const
(nawet reinterpret_cast
)”… naprawdę? Co reinterpret_cast<int *>(reinterpret_cast<uintptr_t>(static_cast<int const *>(0)))
?
reinterpret_cast
często jest to broń z wyboru w przypadku zestawów nieprzezroczystych typów danych API
Służy dynamic_cast
do konwertowania wskaźników / referencji w hierarchii dziedziczenia.
Użyj static_cast
do konwersji zwykłego typu.
Służy reinterpret_cast
do reinterpretacji wzorów bitowych na niskim poziomie. Używaj z dużą ostrożnością.
Użyj const_cast
do rzucania const/volatile
. Unikaj tego, chyba że utkniesz przy użyciu niepoprawnego interfejsu API.
(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);
}
static_cast<char*>(&val)
?
static_cast
dział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 cast
na 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
.
Może to pomóc, jeśli znasz trochę elementów wewnętrznych ...
static_cast
static_cast
dla nich.A
na B
, konstruktor static_cast
wywołań B
przechodzi A
jako param. Alternatywnie A
może mieć operator konwersji (tj A::operator B()
.). Jeśli B
nie ma takiego konstruktora lub A
nie ma operatora konwersji, pojawia się błąd czasu kompilacji.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.A&
się B&
.dynamic_cast
(Base*)
aby (Derived*)
może zakończyć się niepowodzeniem, jeśli wskaźnik nie jest w rzeczywistości typu pochodnej.A*
celu B*
, jeśli obsada jest nieprawidłowy następnie dynamic_cast powróci nullptr.A&
aby B&
jeśli obsada jest nieprawidłowy następnie dynamic_cast rzuci bad_cast wyjątek.const_cast
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.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
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 mutable
może być lepszym wyborem do wdrożenia logicznej stałości.
mutable
, castingiem krzyżowym itp.
Czy to odpowiada na twoje pytanie?
Nigdy nie korzystałem reinterpret_cast
i zastanawiam się, czy trafienie na skrzynkę, która tego potrzebuje, nie jest zapachem złego designu. W bazie kodu, nad którą pracuję, dynamic_cast
używa się dużo. Różnica static_cast
polega na tym, że dynamic_cast
sprawdza środowisko uruchomieniowe , które może (bezpieczniej) lub nie (więcej narzutu) być tym, czego chcesz (patrz msdn ).
reinterpret_cast
do 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); }
reinterpret_cast
, nie ma zbyt wielu zastosowań.
reinterpret_cast
uż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_cast
są używane do przekształcenia tych surowych danych w obiekt.
Oprócz innych dotychczasowych odpowiedzi, tutaj jest nieoczywisty przykład, w którym static_cast
nie jest wystarczający, więc reinterpret_cast
jest 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_cast
musisz 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_cast
dział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);
&static_cast<void*>(pNetFwPolicy2)
zamiast static_cast<void**>(&pNetFwPolicy2)
?
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) var
i 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.
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 ++ :
static_cast
widok kontra dynamic_cast
kontra reinterpret_cast
wewnę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 nullptr
wskazuje nieprawidłowy downcast.
Dlatego jeśli twój kod nie jest w stanie tego sprawdzić nullptr
i podjąć prawidłowej akcji nie przerywania, powinieneś użyć static_cast
zamiast dynamicznego rzutowania.
Jeśli przerwanie jest jedyną czynnością, jaką może wykonać Twój kod, być może chcesz tylko włączyć dynamic_cast
kompilacje debugowania ( -NDEBUG
) i użyć static_cast
inaczej, 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 setarch
jest 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 D
musi 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 D
zawiera w sobie strukturę pamięci zgodną ze strukturą wewnętrzną B1
i B2
wewnę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 D
zostanie 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 B2
w pamięci:
b2s[1] = &d;
poza tym, że ten ma D
zamiast 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_cast
wykonane tam poprawnie obliczyło przesunięcie od pełnej D
struktury danych w 0x7fffffffc930 do B2
podobnej, która jest w 0x7fffffffc940. Wnioskujemy również, że między 0x7fffffffc930 a 0x7fffffffc940 prawdopodobnie są to B1
dane 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 B2
do 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:
D
co nie istniejedynamic_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_cast
dostęp do niektórych dodatkowych struktur danych w pamięci RTTI, które reprezentują hierarchię klas.
Dlatego musi zaczynać od B2
wpisu dla tej tabeli, a następnie przejść tę hierarchię klas, aż stwierdzi, że vtable dla D
typecast z b2s[0]
.
Dlatego reinterpretacja obsady jest potencjalnie droga! Oto przykład, w którym łatka jednowarstwowa przekształcająca dynamic_cast
a static_cast
w złożony projekt skróciła czas działania o 33%! .
reinterpret_cast<D*>(b2s[1]) 0x7fffffffc940
ten po prostu wierzy nam na ślepo: powiedzieliśmy, że jest D
adres 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 -O0
zestawu, 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.