2010-03-04 4 views
7

Il mio programma deve utilizzare il void * per trasportare dati o oggetti in situazione di invocazione dinamica, in modo che possa fare riferimento a dati di tipi arbitrari, anche primitivi tipi. Tuttavia, ho scoperto di recente che il processo di down-casting di questi void * in caso di classi con più classi di base fallisce e addirittura causa l'arresto anomalo del mio programma dopo aver richiamato i metodi su questi puntatori downed cast anche se gli indirizzi di memoria sembrano essere corretti. L'arresto si verifica durante l'accesso a "vtable".ereditarietà multipla: risultato inatteso dopo il cast da void * a 2a classe base

Così ho creato un piccolo banco di prova, l'ambiente è gcc 4.2 su Mac OS X:

class Shape { 
public: 
    virtual int w() = 0; 
    virtual int h() = 0; 
}; 

class Square : public Shape { 
public: 
    int l; 
    int w() {return l;} 
    int h() {return l;} 
}; 

class Decorated { 
public: 
    int padding; 
    int w() {return 2*padding;} 
    int h() {return 2*padding;} 
}; 

class DecoratedSquare : public Square, public Decorated { 
public: 
    int w() {return Square::w() + Decorated::w();} 
    int h() {return Square::h() + Decorated::h();} 
}; 


#include <iostream> 

template <class T> T shape_cast(void *vp) { 
// return dynamic_cast<T>(vp); // not possible, no pointer to class type 
// return static_cast<T>(vp); 
// return T(vp); 
// return (T)vp; 
    return reinterpret_cast<T>(vp); 
} 

int main(int argc, char *argv[]) { 
    DecoratedSquare *ds = new DecoratedSquare; 
    ds->l = 20; 
    ds->padding = 5; 
    void *dsvp = ds; 

    std::cout << "Decorated (direct)" << ds->w() << "," << ds->h() << std::endl; 

    std::cout << "Shape " << shape_cast<Shape*>(dsvp)->w() << "," << shape_cast<Shape*>(dsvp)->h() << std::endl; 
    std::cout << "Square " << shape_cast<Square*>(dsvp)->w() << "," << shape_cast<Square*>(dsvp)->h() << std::endl; 
    std::cout << "Decorated (per void*) " << shape_cast<Decorated*>(dsvp)->w() << "," << shape_cast<Decorated*>(dsvp)->h() << std::endl; 
    std::cout << "DecoratedSquare " << shape_cast<DecoratedSquare*>(dsvp)->w() << "," << shape_cast<DecoratedSquare*>(dsvp)->h() << std::endl; 
} 

produce il seguente output:

Decorated (direct)30,30 
Shape 30,30 
Square 30,30 
Decorated (per void*) 73952,73952 
DecoratedSquare 30,30 

Come si può vedere, la "Decorato (per vuoto *) "il risultato è completamente sbagliato. Dovrebbe anche essere 30,30 come nella prima riga.

Qualunque metodo di cast che utilizzo in shape_cast() otterrò sempre gli stessi risultati imprevisti per la parte Decorated. C'è qualcosa di completamente sbagliato in questi vuoti *.

Dalla mia comprensione di C++ questo dovrebbe essere effettivamente funzionante. C'è qualche possibilità di farlo funzionare con il vuoto *? Può essere un bug in gcc?

Grazie

+1

È possibile utilizzare reinterpret_cast per eseguire il cast a void * e quindi tornare al tipo originale. È possibile __NOT__ eseguire il cast a void * e quindi a qualsiasi altra cosa. –

+0

Sarebbe molto meglio usare il pattern decoratore di MI, non che ciò risolva il cast dal problema void *. – quamrana

risposta

7

Non è un bug del compilatore, è ciò che fa reinterpret_cast. L'oggetto DecoratedSquare sarà presentato in memoria di qualcosa di simile:

Square 
Decorated 
DecoratedSquare specific stuff 

Conversione di un puntatore a questo per void* darà l'indirizzo di inizio di questi dati, senza alcuna conoscenza di ciò che tipo è lì. reinterpret_cast<Decorated*> prenderà quell'indirizzo e interpreterà qualsiasi cosa ci sia come Decorated - ma il contenuto della memoria reale è lo Square. Questo è sbagliato, quindi ottieni un comportamento indefinito.

Si dovrebbe ottenere i risultati corretti se si reinterpret_cast al tipo dinamico corretto (ovvero DecoratedSquare), quindi convertire nella classe base.

+0

Questo lo spiega ma significherebbe che senza la conoscenza della sottoclasse concreta non posso produrre codice che funzioni su puntatori a una delle classi base in modo polimorfico come sarebbe possibile in situazioni di ereditarietà singola, dove il chiamante non bisogno di conoscere la sottoclasse concreta. In altre parole, non sarebbe possibile compilare una struttura e usarla più tardi quando le sottoclassi concrete sono conosciute? Ciò significherebbe che l'ereditarietà multipla in C++ non è completa. –

+2

MI in C++ è completo, a meno che non vengano eliminate le informazioni sul tipo. È possibile utilizzare le funzioni 'virtuali' per ottenere un comportamento polimorfico senza conoscere sottoclassi concrete se non si esegue il cast di tipi errati. –

+0

Per me, dato che non conosco il sottotitolo concreto in quella parte del framework, significa solo che non posso usare void * lì. Devo portare informazioni tipo in qualche modo usando un "qualsiasi" o simili. Non bello e non quello che mi aspettavo :(ma almeno mi hai dato una buona spiegazione Grazie a tutti –

2

Uno static_cast o un dynamic_cast in presenza di ereditarietà multipla può cambiare la rappresentazione del puntatore mediante compensazione in modo che designa l'indirizzo corretto. static_cast determina l'offset corretto considerando le informazioni di tipizzazione statiche. dynamic_cast lo fa controllando il tipo dinamico. Se vai a throw void *, stai perdendo tutte le informazioni di tipizzazione statiche e la possibilità di ottenere informazioni di digitazione dinamiche, quindi il reinterpret_cast che stai utilizzando presuppone che l'offset sia nullo, in mancanza di alcune volte.

+0

, quindi ciò significa che il vuoto * non è fattibile in situazioni di ereditarietà multiple? –

+1

È fattibile, purché si parli sempre del tipo effettivo dell'oggetto, non di una classe base. –

+0

vedere il mio commento sulla risposta di Mike –

9

Ripetere dieci volte: l'unica cosa che si può tranquillamente fare con un puntatore reinterpret_cast è reinterpret_cast riportandolo allo stesso tipo di puntatore da cui proviene. Lo stesso vale per le conversioni su void*: è necessario riconvertire il tipo originale.

Pertanto, se si trasmette un valore da DecoratedSquare* a void*, è necessario trasmetterlo nuovamente a DecoratedSquare*. Non Decorated*, non Square*, non Shape*. Alcuni di questi potrebbero funzionare sulla tua macchina, ma questa è una combinazione di buona fortuna e comportamento specifico dell'implementazione.Di solito funziona con l'ereditarietà singola, perché non vi è alcun motivo ovvio per implementare i puntatori di oggetti in un modo che lo bloccherebbe, ma ciò non è garantito e non può funzionare in generale per l'ereditarietà multipla.

Si dice che il proprio codice accede a "tipi arbitrari, compresi i tipi primitivi" tramite un vuoto *. Non c'è niente di sbagliato in questo - presumibilmente chiunque riceve i dati sa di trattarlo come un DecoratedSquare* e non come, diciamo, int*.

Se chi lo riceve solo sa trattarlo come una classe base, come ad esempio Decorated*, allora chi lo converte in void* dovrebbe static_cast alla classe base, poi a void*:

void *decorated_vp = static_cast<Decorated*>(ds); 

Ora, quando lanci decorated_vp di nuovo a Decorated*, otterrai il risultato di static_cast<Decorated*>(ds), che è quello che ti serve.

+0

Steve grazie per questo. Lo riassumo molto. In effetti, è sempre garantito che nella parte chiamante del framework ci sia un noto tipo di puntatore di base che viene lanciato su void * e poi trasportato all'estremità ricevente, che conosce 2 cose: la classe base più alta e alcune sottoclassi dove un metodo è definito. Non sa, tuttavia, la sottoclasse concreta. Ma ora sembra che tutto sia a posto, dato che il destinatario riceve sempre reinterpret_casts alla stessa classe di base più alta di quella inviata e quindi esegue un dynamic_cast o un static_cast alla sottoclasse intermedia dove invoca il metodo desiderato. –

+0

+1 per "l'unica cosa che puoi tranquillamente fare con un puntatore reinterpret_cast è reinterpret_cast di nuovo allo stesso tipo di puntatore da cui proviene." Riassume molto bene la situazione. –

Problemi correlati