2015-05-12 19 views
5

In una base di codice complesso, ho un array di puntatore classe base non virtuale (la classe base non ha metodi virtuali)C++ pericoloso Cast soluzione

Considerate questo codice:

#include <iostream> 

using namespace std; 

class TBase 
{ 
    public: 
     TBase(int i = 0) : m_iData(i) {} 
     ~TBase(void) {} 

     void Print(void) {std::cout << "Data = " << m_iData << std::endl;} 

    protected: 
     int  m_iData; 
}; 

class TStaticDerived : public TBase 
{ 
    public: 
     TStaticDerived(void) : TBase(1) {} 
     ~TStaticDerived(void) {} 
}; 

class TVirtualDerived : public TBase 
{ 
    public: 
     TVirtualDerived(void) : TBase(2) {} 
     virtual ~TVirtualDerived(void) {} //will force the creation of a VTABLE 
}; 

void PrintType(TBase *pBase) 
{ 
    pBase->Print(); 
} 

void PrintType(void** pArray, size_t iSize) 
{ 
    for(size_t i = 0; i < iSize; i++) 
    { 
     TBase *pBase = (TBase*) pArray[i]; 
     pBase->Print(); 
    } 
} 


int main() 
{ 
    TBase b(0); 
    TStaticDerived sd; 
    TVirtualDerived vd; 

    PrintType(&b); 
    PrintType(&sd); 
    PrintType(&vd); //OK 

    void* vArray[3]; 
    vArray[0] = &b; 
    vArray[1] = &sd; 
    vArray[2] = &vd; //VTABLE not taken into account -> pointer not OK 
    PrintType(vArray, 3); 

    return 0; 
} 

L'uscita è (compilato con Mingw-W64 GCC 4.9.2 on Win64):

Data = 0 
Data = 1 
Data = 2 
Data = 0 
Data = 1 
Data = 4771632 

la ragione del fallimento è che ogni istanza di TVirtualDerived ha un puntatore alla tabella virtuale, che non ha le TBase. Quindi, risalire fino a TBase senza informazioni di tipo precedenti (da void * a TBase *) non è sicuro.

Il fatto è che non posso evitare di lanciare a vuoto * in primo luogo. Aggiunta di un metodo virtuale (distruttore per esempio) sulle opere classe di base, ma ad un costo di memoria (che voglio evitare)

Contesto:

stiamo implementando un sistema di segnale/scanalatura, in modo molto ambiente limitato (memoria severamente limitata). Dal momento che abbiamo diversi milioni di oggetto che può inviare o ricevere segnali, questo tipo di ottimizzazione è efficace (quando funziona, ovviamente)

Domanda:

Come posso risolvere questo problema? Finora, ho trovato:

1 - aggiungi un metodo virtuale in TBase. Funziona, ma in realtà non risolve il problema, lo evita. Ed è inefficiente (troppa memoria)

2 - trasmettere a TBase * invece di trasmettere a vuoto * nell'array, a spese di una perdita di generalità. (probabilmente quello che proverò dopo)

Vedete un'altra soluzione?

+1

Semplicemente il casting in 'TBase *' * prima * il casting in 'void *' risolve il problema in modo soddisfacente? ([Vedi qui] (https : //ideone.com/kpNhe6)) –

+1

Giusto per essere chiari: alcune delle tue classi derivate hanno metodi virtuali. Altri no. E TBase stesso è così piccolo che l'aggiunta di un puntatore vtable causa un significativo aumento delle dimensioni della memoria. Corretta? –

+0

Hai considerato l'utilizzo di modelli? – cup

risposta

3

Devi considerare come la classe è esposta in memoria. TBase è facile, è solo quattro byte con un componente:

_ _ _ _ 
|_|_|_|_| 
^ 
m_iData 

TStaticDerived è lo stesso. Tuttavia, TVirtualDerived è completamente diverso. Ora ha un allineamento di 8 e deve iniziare in anticipo con una vtable, che contiene una voce per il distruttore:

_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
|_|_|_|_|_|_|_|_|_|_|_|_|_|_|_|_| 
^    ^
vtable   m_iData 

Quindi, quando si esegue il cast vd a void* e poi a TBase*, si sta effettivamente reinterpretare le prime quattro byte del tuo vtable (l'indirizzo di offset in ~TVirtualDerived()) come m_iData. La soluzione è di fare prima un static_cast a TBase*, che restituirà un puntatore per correggere punto di partenza di TBase in vd e poi a void*:

vArray[2] = static_cast<TBase*>(&vd); // now, pointer is OK 
+0

Grazie per la risposta. Ho aggiornato la domanda per spiegare perché abbiamo dovuto usare questo sistema. static_cast non funzionerà in caso di ereditarietà multipla. – Seb

+0

@Seb Ho ripristinato la modifica in quanto è un muro di testo che non modifica sensibilmente lo spirito della domanda. Anche 'static_cast' funziona sicuramente con l'ereditarietà multipla - il problema è probabile che si stia eseguendo il cast avanti e indietro per" void * "non corretto. – Barry

4

Il problema è in lanci. Poiché si utilizza un cast di tipo C tramite il n., è equivalente a reinterpret_cast, che può risultare scadente durante la sottoclasse. Nella prima parte, il tipo è accessibile al compilatore e i tuoi cast sono equivalenti a static_cast.

Ma non riesco a capire perché dici che tu non puoi evitare di trasmettere a void * in primo luogo. Poiché PrintType internamente convertirà lo void * in un TBase *, potresti anche passare uno TBase **. In questo caso funzionerà bene:

void PrintType(TBase** pArray, size_t iSize) 
{ 
    for(size_t i = 0; i < iSize; i++) 
    { 
     TBase *pBase = pArray[i]; 
     pBase->Print(); 
    } 
} 
... 
    TBase* vArray[3]; 
    vArray[0] = &b; 
    vArray[1] = &sd; 
    vArray[2] = &vd; //VTABLE not taken into account -> pointer not OK 
    PrintType(vArray, 3); 

In alternativa, se si desidera ad utilizzare un array void **, è necessario esplicitamente fare in modo che ciò che si mette in esso sono solo TBase *e non puntatore al sottoclassi:

void* vArray[3]; 
vArray[0] = &b; 
vArray[1] = static_cast<TBase *>(&sd); 
vArray[2] = static_cast<TBase *>(&vd); 
PrintType(vArray, 3); 

Quelli sia trasmesso correttamente metodo:

Data = 0 
Data = 1 
Data = 2 
Data = 0 
Data = 1 
Data = 2 
+0

Il codice fornito era per l'illustrazione del problema (esempio minimo). Nel nostro codebase (> 800 000 righe di codice), abbiamo una classe di delegati senza tipo (vedere questo articolo per l'idea principale: [delegati] (http://www.codeproject.com/Articles/7150/Member-Function-Pointers -e-la-veloce-Possible)). La trasmissione a TBase funziona per la maggior parte dei casi, ma quando si utilizza l'ereditarietà multipla, non funziona più - abbiamo bisogno del tipo più derivato originale, o il compilatore non sarà in grado di dedurre correttamente l'indirizzo del metodo. Questo è il motivo per cui gettiamo il puntatore dell'oggetto a vuoto - per mantenere il puntatore originale. – Seb

0

Dimentica il polimorfismo virtuale. Fallo alla vecchia maniera.

Aggiungere un byte a ciascun TBase per indicare il tipo e un'istruzione switch nel metodo di stampa su "Do The Right Thing". (questo consente di risparmiare dimensione (puntatore) -1 byte per TBase rispetto all'approccio metodo virtuale

Se l'aggiunta di un byte è ancora troppo costosa, considerare l'uso di campi bit C/C++ (chiunque ricordi quelli (grin)) per spremere il digita il campo in un altro campo che non riempie lo spazio disponibile (ad esempio un numero intero senza segno che ha un valore massimo di 2^24 - 1)

Il codice sarà brutto, vero, ma i tuoi severi vincoli di memoria sono Brutto codice che funziona è meglio di un bel codice che non riesce.

+0

Grazie per la risposta. Abbiamo provato questo approccio e funziona per la maggior parte delle classi. Ma abbiamo un problema quando vogliamo derivare da TBase e un'altra classe (da un'altra libreria) con metodi virtuali. Ecco perché inserisco la domanda. Proviamo a fare codice efficiente, ed è già molto brutto! (ma efficiente!). Il problema con brutto è la manutenzione: è costoso (richiede più tempo).E quando avremo bisogno di espandere/patchare il codice dopo 6 mesi, ce ne pentiremo. – Seb