Quando utilizzare static_cast, dynamic_cast, const_cast e reinterpret_cast?


2496

Quali sono gli usi corretti di:

  • static_cast
  • dynamic_cast
  • const_cast
  • reinterpret_cast
  • Cast in stile C. (type)value
  • Cast di stile funzionale type(value)

Come si decide quale utilizzare in quali casi specifici?



3
Per alcuni esempi concreti utili sull'uso di diversi tipi di cast, puoi controllare la prima risposta su una domanda simile in questo altro argomento .
TeaMonkie,

2
Puoi trovare risposte davvero buone per la tua domanda sopra. Ma vorrei aggiungere un altro punto qui, @ e.James "Non c'è nulla che questi nuovi operatori di cast c ++ possano fare e il cast di stile c non possa farlo. Questi vengono aggiunti più o meno per una migliore leggibilità del codice."
BreakBadSP

@BreakBadSP I nuovi cast non sono solo per una migliore leggibilità del codice. Sono lì per rendere più difficile fare cose pericolose, come gettare via const o lanciare puntatori invece dei loro valori. static_cast ha molte meno possibilità di fare qualcosa di pericoloso rispetto al cast in stile ac!
Quarantadue

@FourtyTwo concordato
BreakBadSP il

Risposte:


2571

static_castè il primo cast che dovresti provare a usare. Fa cose come conversioni implicite tra tipi (come intto floato pointer to void*) e può anche chiamare funzioni di conversione esplicite (o implicite). In molti casi, static_castnon è necessario dichiarare esplicitamente , ma è importante notare che la T(something)sintassi è equivalente (T)somethinge dovrebbe essere evitata (ne parleremo più avanti). A T(something, something_else)è sicuro, tuttavia, e garantito per chiamare il costruttore.

static_castpuò anche eseguire il cast delle gerarchie ereditarie. Non è necessario quando si lancia verso l'alto (verso una classe base), ma quando si lancia verso il basso può essere utilizzato purché non passi attraverso l' virtualeredità. Tuttavia, non esegue il controllo ed è un comportamento indefinito static_castridurre una gerarchia a un tipo che non è effettivamente il tipo di oggetto.


const_castpuò essere usato per rimuovere o aggiungere consta una variabile; nessun altro cast C ++ è in grado di rimuoverlo (nemmeno reinterpret_cast). È importante notare che la modifica di un constvalore precedente non è definita solo se la variabile originale è const; se lo usi per togliere constun riferimento a qualcosa con cui non è stato dichiarato const, è sicuro. Ciò può essere utile quando si sovraccaricano le funzioni dei membri basate const, ad esempio. Può anche essere utilizzato per aggiungere consta un oggetto, ad esempio per chiamare un sovraccarico della funzione membro.

const_castfunziona anche allo stesso modo volatile, sebbene sia meno comune.


dynamic_castè utilizzato esclusivamente per la gestione del polimorfismo. È possibile eseguire il cast di un puntatore o riferimento a qualsiasi tipo polimorfico a qualsiasi altro tipo di classe (un tipo polimorfico ha almeno una funzione virtuale, dichiarata o ereditata). Puoi usarlo per più di un semplice lancio verso il basso: puoi lanciare lateralmente o anche su un'altra catena. Il dynamic_castcercherà l'oggetto desiderato e tornare, se possibile. In caso contrario, verrà restituito nullptrnel caso di un puntatore, oppure verrà lanciato std::bad_castnel caso di un riferimento.

dynamic_castha alcune limitazioni, però. Non funziona se ci sono più oggetti dello stesso tipo nella gerarchia dell'ereditarietà (il cosiddetto "diamante temuto") e non stai usando l' virtualereditarietà. Inoltre può solo passare attraverso l'eredità pubblica - non riuscirà sempre a viaggiare attraverso protectedo privateeredità. Questo è raramente un problema, tuttavia, poiché tali forme di eredità sono rare.


reinterpret_castè il cast più pericoloso e dovrebbe essere usato con parsimonia. Trasforma un tipo direttamente in un altro, come ad esempio lanciare il valore da un puntatore a un altro o archiviare un puntatore in una into altre cose brutte. In gran parte, l'unica garanzia che ottieni reinterpret_castè che normalmente se restituisci il risultato al tipo originale, otterrai lo stesso valore esatto (ma non se il tipo intermedio è più piccolo del tipo originale). Esistono anche numerose conversioni che reinterpret_castnon possono essere eseguite. Viene utilizzato principalmente per conversioni e manipolazioni di bit particolarmente strane, come trasformare un flusso di dati non elaborati in dati reali o archiviare i dati nei bit bassi di un puntatore in dati allineati.


Cast di tipo C e cast di tipo funzione sono calchi utilizzando (type)objecto type(object), rispettivamente, e sono funzionalmente equivalenti. Sono definiti come il primo dei seguenti che ha esito positivo:

  • const_cast
  • static_cast (sebbene ignorando le restrizioni di accesso)
  • static_cast (vedi sopra), quindi const_cast
  • reinterpret_cast
  • reinterpret_cast, poi const_cast

Può quindi essere usato come sostituto di altri cast in alcuni casi, ma può essere estremamente pericoloso a causa della capacità di devolvere in un reinterpret_cast, e quest'ultimo dovrebbe essere preferito quando è necessario il cast esplicito, a meno che non si sia sicuri che static_castavrà successo o reinterpret_castfallirà . Anche allora, considera l'opzione più lunga ed esplicita.

I lanci di tipo C ignorano anche il controllo di accesso quando eseguono un static_cast, il che significa che hanno la capacità di eseguire un'operazione che nessun altro cast può fare. Questo è principalmente un problema, e nella mia mente è solo un altro motivo per evitare i cast in stile C.


17
dynamic_cast è solo per tipi polimorfici. devi usarlo solo quando esegui il casting in una classe derivata. static_cast è sicuramente la prima opzione a meno che non sia necessaria la funzionalità di dynamic_cast. Non è un miracoloso "cast di controllo del tipo" da proiettile d'argento in generale.
jalf

2
Bella risposta! Un'osservazione rapida: static_cast potrebbe essere necessario per eseguire il cast della gerarchia nel caso in cui si abbia un Derivato * e per eseguire il cast in Base * e, poiché i doppi puntatori / riferimenti non eseguono automaticamente il cast della gerarchia. Mi sono imbattuto in una situazione del genere (francamente, non comune) due minuti fa. ;-)
bartgol

5
* "nessun altro cast C ++ è in grado di rimuovere const(nemmeno reinterpret_cast)" ... davvero? Che dire reinterpret_cast<int *>(reinterpret_cast<uintptr_t>(static_cast<int const *>(0)))?
user541686,

29
Penso che un dettaglio importante che manca sopra sia che dynamic_cast abbia una penalità di runtime rispetto a static o reinterpret_cast. Questo è importante, ad esempio nel software in tempo reale.
jfritz42,

5
Vale la pena ricordare che reinterpret_castè spesso l'arma preferita quando si ha a che fare con una serie di tipi di dati opachi di un'API
Scheletro di classe

333

Utilizzare dynamic_castper convertire puntatori / riferimenti all'interno di una gerarchia di ereditarietà.

Utilizzare static_castper conversioni di tipo ordinario.

Utilizzare reinterpret_castper la reinterpretazione a basso livello di modelli di bit. Utilizzare con estrema cautela.

Utilizzare const_castper gettare via const/volatile. Evita questo a meno che non ti blocchi con un'API const-errata.


2
Fai attenzione con dynamic_cast. Si basa su RTTI e questo non funzionerà come previsto oltre i confini delle librerie condivise. Semplicemente perché si crea una libreria eseguibile e condivisa in modo indipendente, non esiste un modo standardizzato per sincronizzare RTTI tra build diverse. Per questo motivo nella libreria Qt esiste qobject_cast <> che utilizza le informazioni sul tipo QObject per il controllo dei tipi.
user3150128,

198

(Molte spiegazioni teoriche e concettuali sono state fornite sopra)

Di seguito sono riportati alcuni esempi pratici quando ho usato static_cast , dynamic_cast , const_cast , reinterpret_cast .

(Fa riferimento anche a questo per comprendere la spiegazione: 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);
}

31
La teoria di alcune delle altre risposte è buona, ma ancora confusa, vedere questi esempi dopo aver letto le altre risposte ha davvero senso. Cioè senza gli esempi, non ero ancora sicuro, ma con loro, ora sono sicuro di cosa significano le altre risposte.
Solx,

1
Informazioni sull'ultimo utilizzo di reinterpret_cast: non è lo stesso che usare static_cast<char*>(&val)?
Lorenzo Belli,

3
@LorenzoBelli Certo che no. Hai provato? Quest'ultimo non è C ++ valido e blocca la compilazione. static_castfunziona solo tra tipi con conversioni definite, relazione visibile per eredità o da / a void *. Per tutto il resto, ci sono altri cast. reinterpret casta qualsiasi char *tipo è consentito consentire la lettura della rappresentazione di qualsiasi oggetto - e uno dei pochi casi in cui tale parola chiave è utile, non un generatore dilagante di implementazione / comportamento indefinito. Ma questa non è considerata una conversione "normale", quindi non è consentita dal (di solito) molto conservatore static_cast.
underscore_d

2
reinterpret_cast è piuttosto comune quando si lavora con software di sistema come i database. Nella maggior parte dei casi scrivi il tuo gestore di pagine che non ha idea di quale sia il tipo di dati archiviato nella pagina e restituisce solo un puntatore vuoto. Sta ai livelli più alti fare un cast reinterpretare e inferirlo come quello che vogliono.
Sohaib,

1
L'esempio const_cast mostra un comportamento indefinito. Una variabile dichiarata come const non può essere de-const-ed. Tuttavia, una variabile dichiarata come non costante che viene passata a una funzione che prende un riferimento const può in quella funzione essere decifrata senza che sia UB.
Johann Gerell,

99

Potrebbe essere utile se conosci un po 'di interni ...

static_cast

  • Il compilatore C ++ sa già come convertire tra tipi di scaler come float in int. Usa static_castper loro.
  • Quando chiedi al compilatore di convertire da tipo Aa B, il costruttore delle static_castchiamate Bpassa Acome parametro. In alternativa, Apotrebbe avere un operatore di conversione (ad es A::operator B().). Se Bnon ha tale costruttore o Anon ha un operatore di conversione, viene visualizzato un errore di tempo di compilazione.
  • Fusioni da A*al B*riesce sempre se A e B sono in gerarchia di ereditarietà (o nulla) altrimenti si ottiene errore di compilazione.
  • Gotcha : se si lancia il puntatore di base sul puntatore derivato ma se l'oggetto reale non è realmente derivato, non si ottiene alcun errore. Si ottiene un puntatore errato e molto probabilmente un segfault in fase di esecuzione. Lo stesso vale per A&a B&.
  • Gotcha : il cast da Derived to Base o viceversa crea una nuova copia! Per le persone che provengono da C # / Java, questa può essere una grande sorpresa perché il risultato è fondamentalmente un oggetto tagliato creato da Derived.

dynamic_cast

  • dynamic_cast utilizza le informazioni sul tipo di runtime per capire se il cast è valido. Ad esempio, (Base*)per (Derived*)può riuscire se il puntatore non è effettivamente di tipo derivato.
  • Ciò significa che dynamic_cast è molto costoso rispetto a static_cast!
  • Per A*a B*, se cast non è valido, dynamic_cast restituirà nullptr.
  • Per A&per B&se cast è valido quindi dynamic_cast lancerà un'eccezione bad_cast.
  • A differenza di altri cast, c'è un sovraccarico di runtime.

const_cast

  • Mentre static_cast può fare non-const per const, non può andare diversamente. Const_cast può fare entrambe le cose.
  • Un esempio in cui ciò risulta utile è l'iterazione attraverso alcuni contenitori come i set<T>quali restituisce solo i suoi elementi come const per assicurarsi di non cambiare la sua chiave. Tuttavia, se il tuo intento è quello di modificare i membri non chiave dell'oggetto, allora dovrebbe essere ok. È possibile utilizzare const_cast per rimuovere la costanza.
  • Un altro esempio è quando si desidera implementare T& SomeClass::foo()e const T& SomeClass::foo() const. Per evitare la duplicazione del codice, è possibile applicare const_cast per restituire il valore di una funzione da un'altra.

reinterpret_cast

  • Questo in sostanza dice che prendere questi byte in questa posizione di memoria e pensarlo come un dato oggetto.
  • Ad esempio, puoi caricare 4 byte di float su 4 byte di int per vedere come appaiono i bit in float.
  • Ovviamente, se i dati non sono corretti per il tipo, potresti ottenere segfault.
  • Non vi è alcun sovraccarico di runtime per questo cast.

Ho aggiunto le informazioni sull'operatore di conversione, ma ci sono alcune altre cose che dovrebbero essere risolte e non mi sento così a mio agio ad aggiornarlo troppo. Gli articoli sono: 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.Ottieni UB che può comportare un segfault in fase di esecuzione se sei fortunato. 2. I lanci dinamici possono essere utilizzati anche nel cross casting. 3. In alcuni casi i cast di costanti possono causare UB. L'uso mutablepuò essere una scelta migliore per implementare la costanza logica.
Adrian,

1
@Adrian hai ragione sotto tutti gli aspetti. La risposta è scritta per le persone a livello più o meno principiante e non volevo sopraffarli con tutte le altre complicazioni che ne derivano mutable, il casting incrociato ecc.
Shital Shah,

16

Ha questa risposta alla domanda?

Non ho mai usato reinterpret_cast, e mi chiedo se incappare in un caso che ne abbia bisogno non è un odore di cattivo design. Nella base di codice su cui lavoro dynamic_castè molto usata. La differenza static_castè che a dynamic_castesegue un controllo di runtime che può (più sicuro) o meno (più overhead) essere ciò che si desidera (vedere msdn ).


3
Ho usato reintrepret_cast per un solo scopo: ottenere i bit da un doppio (stessa dimensione della lunghezza della mia piattaforma).
Giosuè,

2
reinterpret_cast è necessario ad es. per lavorare con oggetti COM. CoCreateInstance () ha un parametro di output di tipo void ** (l'ultimo parametro), in cui passerai il puntatore dichiarato come ad esempio "INetFwPolicy2 * pNetFwPolicy2". Per farlo, devi scrivere qualcosa come reinterpret_cast <void **> (& pNetFwPolicy2).
Serge Rogatch,

1
Forse esiste un approccio diverso, ma io uso reinterpret_castper estrarre pezzi di dati da un array. Ad esempio, se ho char*un grande buffer pieno di dati binari compressi che devo spostare e ottenere singole primitive di vario tipo. Qualcosa del genere: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,

Non l'ho mai usato reinterpret_cast, non ci sono molti usi per questo.
Pika il Mago delle Balene,

Personalmente ho mai visto reinterpret_castusato solo per un motivo. Ho visto dati di oggetti grezzi archiviati in un tipo di dati "BLOB" in un database, quindi quando i dati vengono recuperati dal database, reinterpret_castvengono utilizzati per trasformare questi dati grezzi nell'oggetto.
ImaginaryHuman072889,

15

Oltre alle altre risposte finora, qui è un esempio evidente dove static_castnon è sufficiente, quindi reinterpret_castè necessario. Supponiamo che ci sia una funzione che in un parametro di output restituisce puntatori a oggetti di classi diverse (che non condividono una classe base comune). Un vero esempio di tale funzione è CoCreateInstance()(vedi l'ultimo parametro, che è in realtà void**). Supponiamo che tu richieda una particolare classe di oggetti da questa funzione, quindi conosci in anticipo il tipo per il puntatore (cosa che fai spesso per gli oggetti COM). In questo caso non è possibile eseguire il cast del puntatore al puntatore void**con static_cast: è necessario reinterpret_cast<void**>(&yourPointer).

Nel codice:

#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) );

Tuttavia, static_castfunziona per semplici puntatori (non puntatori a puntatori), quindi il codice sopra può essere riscritto per evitare reinterpret_cast(a un prezzo di una variabile aggiuntiva) nel modo seguente:

#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);

Non funzionerebbe qualcosa del genere &static_cast<void*>(pNetFwPolicy2)invece di static_cast<void**>(&pNetFwPolicy2)?
jp48,

9

Mentre altre risposte descrivono bene tutte le differenze tra i cast C ++, vorrei aggiungere una breve nota del perché non dovresti usare i cast in stile C (Type) vare Type(var).

Per i principianti C ++ i cast in stile C sembrano essere l'operazione superset sui cast C ++ (static_cast <> (), dynamic_cast <> (), const_cast <> (), reinterpret_cast <> ()) e qualcuno potrebbe preferirli ai cast C ++ . In effetti il ​​cast in stile C è il superset e il più breve da scrivere.

Il problema principale dei cast in stile C è che nascondono la vera intenzione dello sviluppatore del cast. I cast in stile C possono eseguire praticamente tutti i tipi di cast da cast normalmente sicuri eseguiti da static_cast <> () e dynamic_cast <> () a cast potenzialmente pericolosi come const_cast <> (), dove il modificatore const può essere rimosso in modo che le variabili const può essere modificato e reinterpret_cast <> () che può anche reinterpretare i valori interi nei puntatori.

Ecco il campione.

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.

Il motivo principale per cui i cast di C ++ sono stati aggiunti al linguaggio è stato quello di consentire a uno sviluppatore di chiarire le sue intenzioni - perché farà quel cast. Usando i cast in stile C che sono perfettamente validi in C ++, stai rendendo il tuo codice meno leggibile e più soggetto a errori soprattutto per gli altri sviluppatori che non hanno creato il tuo codice. Quindi, per rendere il tuo codice più leggibile ed esplicito, dovresti sempre preferire i cast C ++ rispetto ai cast in stile C.

Ecco una breve citazione dal libro di Bjarne Stroustrup (l'autore del C ++) The C ++ Programming Language 4th edition - page 302.

Questo cast in stile C è molto più pericoloso degli operatori di conversione nominati perché la notazione è più difficile da individuare in un programma di grandi dimensioni e il tipo di conversione previsto dal programmatore non è esplicito.


5

Per capire, consideriamo di seguito lo snippet di codice:

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;
}

Solo la riga (4) viene compilata senza errori. Solo reinterpret_cast può essere utilizzato per convertire un puntatore a un oggetto in un puntatore a qualsiasi tipo di oggetto non correlato.

Uno da notare è: dynamic_cast non funzionerebbe in fase di esecuzione, tuttavia sulla maggior parte dei compilatori non riuscirà anche a compilare perché non ci sono funzioni virtuali nella struttura del puntatore da trasmettere, il che significa che dynamic_cast funzionerà solo con puntatori di classe polimorfici .

Quando utilizzare il cast C ++ :

  • Utilizzare static_cast come l'equivalente di un cast in stile C che valuti la conversione o quando è necessario eseguire il cast esplicito di un puntatore da una classe alla sua superclasse.
  • Usa const_cast per rimuovere il qualificatore const.
  • Utilizzare reinterpret_cast per eseguire conversioni non sicure di tipi di puntatore da e verso numeri interi e altri tipi di puntatore. Usalo solo se sappiamo cosa stiamo facendo e comprendiamo i problemi di aliasing.

3

static_castvs dynamic_castvs reinterpret_castinternals visualizza su un downcast / upcast

In questa risposta, voglio confrontare questi tre meccanismi su un esempio concreto di upcast / downcast e analizzare cosa succede ai puntatori / memoria / assembly sottostanti per dare una comprensione concreta di come si confrontano.

Credo che questo darà una buona intuizione su come questi cast siano diversi:

  • static_cast: esegue un offset dell'indirizzo in fase di runtime (basso impatto di runtime) e non verifica che un downcast sia corretto.

  • dyanamic_cast: esegue lo stesso offset dell'indirizzo durante il runtime static_cast, ma anche un costoso controllo di sicurezza che un downcast è corretto utilizzando RTTI.

    Questo controllo di sicurezza consente di eseguire una query se un puntatore della classe base è di un determinato tipo in fase di esecuzione controllando un valore restituito nullptrche indica un downcast non valido.

    Pertanto, se il tuo codice non è in grado di verificarlo nullptre di intraprendere un'azione valida di non interruzione, dovresti semplicemente utilizzare static_castinvece del cast dinamico.

    Se un abort è l'unica azione che il tuo codice può intraprendere, forse vuoi solo abilitare dynamic_castin debug builds ( -NDEBUG), e usare static_castaltrimenti, ad esempio come fatto qui , per non rallentare le tue corse veloci.

  • reinterpret_cast: non fa nulla in fase di esecuzione, nemmeno l'offset dell'indirizzo. Il puntatore deve puntare esattamente al tipo corretto, nemmeno una classe base funziona. In genere non lo si desidera a meno che non siano coinvolti flussi di byte non elaborati.

Considera il seguente esempio di codice:

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;
}

Compila, esegui e disassembla con:

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

dove setarchviene utilizzato per disabilitare ASLR per facilitare il confronto delle esecuzioni.

Uscita possibile:

&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

Ora, come menzionato in: https://en.wikipedia.org/wiki/Virtual_method_table per supportare in modo efficiente le chiamate al metodo virtuale, la struttura dei dati di memoria Ddeve assomigliare a:

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

Il fatto chiave è che la struttura dei dati di memoria di Dcontiene al suo interno una struttura di memoria compatibile con quella di B1e quella B2interna.

Pertanto giungiamo alla conclusione critica:

un upcast o downcast deve solo spostare il valore del puntatore di un valore noto al momento della compilazione

In questo modo, quando Dviene passato all'array del tipo di base, il cast di tipo calcola effettivamente quell'offset e punta a qualcosa che sembra esattamente un valido B2in memoria:

b2s[1] = &d;

tranne che questo ha la vtable per Dinvece di B2, e quindi tutte le chiamate virtuali funzionano in modo trasparente.

Ora possiamo finalmente tornare al tipo di casting e all'analisi del nostro esempio concreto.

Dall'output stdout vediamo:

&d           0x7fffffffc930
b2s[1]       0x7fffffffc940

Pertanto, l'implicito static_castfatto lì ha calcolato correttamente l'offset dalla Dstruttura dati completa a 0x7fffffffc930 a quella B2simile che è a 0x7fffffffc940. Ne deduciamo anche che ciò che sta tra 0x7fffffffc930 e 0x7fffffffc940 è probabilmente i B1dati e la vtable.

Quindi, nelle sezioni downcast, ora è facile capire come falliscono quelli non validi e perché:

  • static_cast<D*>(b2s[0]) 0x7fffffffc910: il compilatore ha appena aumentato 0x10 in fase di compilazione byte per provare a passare da B2a al contenimentoD

    Ma poiché b2s[0]non era un D, ora indica una regione di memoria indefinita.

    Lo smontaggio è:

    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)

    quindi vediamo che GCC fa:

    • controlla se il puntatore è NULL e se sì restituisce NULL
    • in caso contrario, sottrarre 0x10 da esso per raggiungere il Dquale non esiste
  • dynamic_cast<D*>(b2s[0]) 0: C ++ in realtà ha scoperto che il cast non era valido e restituito nullptr!

    Non è possibile farlo al momento della compilazione e lo confermeremo dallo smontaggio:

    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)

    Innanzitutto c'è un controllo NULL e restituisce NULL se l'ingresso è NULL.

    Altrimenti, imposta alcuni argomenti in RDX, RSI e RDI e chiama __dynamic_cast.

    Non ho la pazienza di analizzarlo ulteriormente ora, ma come altri hanno detto, l'unico modo per farlo funzionare è __dynamic_castquello di accedere ad alcune strutture di dati in memoria RTTI extra che rappresentano la gerarchia di classi.

    Deve quindi iniziare dalla B2voce per quella tabella, quindi seguire questa gerarchia di classi fino a quando trova la vtable per un Dtypecast b2s[0].

    Ecco perché reinterpretare il cast è potenzialmente costoso! Ecco un esempio in cui una patch di una riga che converte a dynamic_castin static_castin un progetto complesso ha ridotto il tempo di esecuzione del 33%! .

  • reinterpret_cast<D*>(b2s[1]) 0x7fffffffc940questo ci crede ciecamente: abbiamo detto che esiste un Dindirizzo b2s[1]e il compilatore non esegue calcoli di offset.

    Ma questo è sbagliato, poiché D è in realtà a 0x7fffffffc930, ciò che è a 0x7fffffffc940 è la struttura simile a B2 all'interno di D! Quindi si accede alla spazzatura.

    Possiamo confermarlo dall'assemblea orrenda -O0che sposta semplicemente il valore:

    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)

Domande correlate:

Testato su Ubuntu 18.04 amd64, GCC 7.4.0.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.