2010-05-24 11 views
6

La situazione è la seguente. Ho libreria condivisa, che contiene la definizione di classe -Perché la funzione virtuale C++ definita nell'intestazione non può essere compilata e collegata in vtable?

QueueClass : IClassInterface 
{ 
    virtual void LOL() { do some magic} 
} 

La mia libreria condivisa inizializzare membro della classe

QueueClass *globalMember = new QueueClass(); 

Biblioteca quota funzione di esportazione C che restituisce puntatore a globalMember -

void * getGlobalMember(void) { return globalMember;} 

mio l'applicazione utilizza globalMember come questo

((IClassInterface*)getGlobalMember())->LOL(); 

Ora il materiale molto interessante - se non faccio riferimento a LOL dalla libreria condivisa, allora LOL non è collegato e chiamandolo dall'applicazione solleva un'eccezione. Motivo: VTABLE contiene nul al posto del puntatore alla funzione LOL().

Quando sposto la definizione di LOL() dal file .h in .cpp, improvvisamente appare in VTABLE e tutto funziona alla grande. Cosa spiega questo comportamento ?! (gcc compiler + ARM architecture_)

+0

E le altre funzioni virtuali non inline in 'QueueClass' (se ce ne sono)? Lavorano? In altre parole, il problema con 'LOL' local a' LOL's vtable o è l'intero vtable vuoto o mancante? – AnT

+0

perché privatamente ereditare da IClassInterface? –

+2

Perché la funzione 'get ...' restituisce 'void * 'invece di' IClassInterface * '? – AnT

risposta

6

Il linker è il colpevole qui. Quando una funzione è in linea ha più definizioni, una in ogni file cpp a cui viene fatto riferimento. Se il tuo codice non fa mai riferimento alla funzione non viene mai generato.

Tuttavia, il layout vtable viene determinato in fase di compilazione con la definizione della classe. Il compilatore può facilmente dire che lo LOL() è una funzione virtuale e deve avere una voce nel vtable.

Quando arriva il tempo di collegamento per l'app, tenta di compilare tutti i valori dello QueueClass::_VTABLE ma non trova una definizione di LOL() e lo lascia vuoto (nulla).

La soluzione è di fare riferimento a LOL() in un file nella libreria condivisa. Qualcosa di semplice come &QueueClass::LOL;. Potrebbe essere necessario assegnarlo a una variabile throw-away per far sì che il compilatore smetta di lamentarsi delle affermazioni senza alcun effetto.

+0

Ciò significa che il compilatore + linker è semplicemente folle! Non devono mai consentire di creare un oggetto vivente in fase di esecuzione la cui tabella virtuale non è completamente definita. In tal caso il linker potrebbe generare un errore "irrisolto" – valdo

+0

Il compilatore sta semplicemente ottimizzando. Se non usi mai la funzione, perché dovrebbe costruirne una. Se il compilatore emettesse il codice per ogni funzione inline per ogni file, i tempi di compilazione sarebbero enormi, anche i tempi di collegamento sarebbero enormi (il linker dovrebbe essere in grado di rielaborare le funzioni duplicate senza riferimenti). –

+0

Il 'non risolto' viene normalmente generato quando la libreria viene caricata, non quando viene creata, in modo da avere la possibilità di fare riferimento a simboli da un'altra libreria. –

1

La mia ipotesi è che GCC stia cogliendo l'opportunità di inline la chiamata a LOL. Vedrò se riesco a trovare un riferimento per te su questo ...

Vedo disattiva mi ha battuto per una descrizione più approfondita e non ho potuto google sul riferimento che stavo cercando. Quindi lo lascerò a quello.

+0

Sì, se gcc non vede alcuna funzione virtuale non in linea in una classe, non emetterà il vtable per esso . http://gcc.gnu.org/faq.html # vtables –

+0

Grazie, questo aiuta. Ricordo solo di aver letto una bella recensione su questo argomento, ma Teh Great Gizoogle mi ha deluso in questo momento, dal momento che non riesco a trovare la giusta combinazione di parole chiave per trovarlo. È abbastanza brutto che non riesco a mantenere le cose nella mia mente, quando anche il mio cervello virtuale perde, che mi lascia in uno stato piuttosto spiacevole ;-) – Ukko

+0

Whoohoo, ho anche ottenuto il mio primo neg per una risposta con questo ! Ammetto che non è stato il mio lavoro migliore, e per gli interessati il ​​+1 seguito dal -1 compensato a +8. Mi sono sempre chiesto se avrebbe funzionato in quel modo o se avessi perso l'originale +10, ora lo sappiamo ;-) – Ukko

0

Le funzioni definite nei file di intestazione sono allineate durante l'utilizzo. Non sono compilati come parte della libreria; invece dove viene effettuata la chiamata, il codice della funzione sostituisce semplicemente il codice della chiamata, e questo è ciò che viene compilato.

Quindi, non sono sorpreso di vedere che non stai trovando una voce di v-table (a cosa punterebbe?), E non mi sorprende vedere lo spostamento della definizione della funzione in un file .cpp improvvisamente fa funzionare le cose. Sono un po 'sorpreso dal fatto che creare un'istanza dell'oggetto con una chiamata nella libreria faccia la differenza, comunque.

Non sono sicuro se è la fretta, ma dal codice fornito IClassInterface non contiene necessariamente LOL, solo QueueClass. Ma stai lanciando su un puntatore IClassInterface per effettuare la chiamata LOL.

+3

Questo è un ragionamento fasullo. Solo perché una funzione è in linea non significa che non puoi prendere il suo indirizzo. "A cosa punterebbe" è il problema del compilatore, non il problema dell'utente. Normalmente, i compilatori generano un corpo autonomo per una funzione. Questo è quello che avrebbe dovuto fare anche in questo caso. In altre parole, non ci sono problemi con il codice/approccio stesso. – AnT

+0

Beh, non è completamente falso. Quando torni a C giorni a inserire il codice in un'intestazione è stata una ricetta totale per il disastro, ora le cose vanno meglio; tuttavia, ci sono ancora problemi con dove il compilatore dovrebbe emettere quel codice. Sotto il tuo modello che include un file di intestazione risulterà nell'emettere una versione non inline di molti metodi, risultando in file di oggetti enormi e molti linker funzionano senza alcun guadagno. (Questo è un grosso problema quando i modelli sono inclusi.) Tutto questo deriva dal problema di root della compilazione separata basata su file ... sono stati fatti dei compromessi, mi dispiace per quello. – Ukko

+0

Trovo che il passaggio di un 'void *' e gira intorno a un bel problema, specialmente quando viene data la catena di cast: 'QueueClass *' -> 'void *' -> 'IClassInterface *';) –

0

Se questo esempio è semplificato e l'albero di ereditarietà corrente utilizza l'ereditarietà multipla, questo potrebbe essere facilmente spiegato. Quando si esegue un typecast su un puntatore a oggetti, il compilatore deve regolare il puntatore in modo che venga fatto riferimento al vtable corretto. Poiché stai restituendo un void *, il compilatore non ha le informazioni necessarie per eseguire la regolazione.

Edit: Non esiste uno standard per C++ layout di oggetto, ma per un esempio di come l'ereditarietà multipla potrebbe funzionare vedi questo articolo di Bjarne Stroustrup stesso: http://www-plan.cs.colorado.edu/diwan/class-papers/mi.pdf

Se questo è davvero il problema, potrebbe essere in grado di risolvere il problema con una semplice modifica:

IClassInterface *globalMember = new QueueClass(); 

il compilatore C++ farà le modifiche necessarie puntatore quando fa il compito, in modo che la funzione C può restituire il puntatore corretto.

+0

Come funziona l'ereditarietà multipla mondo reale? Come vengono creati i vtables? Dì: multiplo di classe: interface1, interface2; Come sarà vtable questo mostro ??? Sono davvero confuso. –

+0

@ 0xDEAD BEEF, vedi il mio aggiornamento. –

2

Non sono d'accordo con @sechastain.

Inlining è lungi dall'essere automatico. Indipendentemente dal fatto che il metodo sia definito in posizione o un suggerimento (inline parola chiave o __forceinline), il compilatore è l'unico a decidere se l'inlining avrà effettivamente luogo e utilizza euristiche complicate per farlo. Un caso particolare, tuttavia, è che non deve effettuare la chiamata in linea quando viene invocato un metodo virtuale utilizzando la distribuzione runtime, proprio perché la distribuzione e l'inlining del runtime non sono compatibili.

Per comprendere la precisione di "usare runtime spedizione":

IClassInterface* i = /**/; 
i->LOL();     // runtime dispatch 
i->QueueClass::LOL();  // compile time dispatch, inline is possible 

@0xDEAD BEEF: Trovo il vostro disegno fragile per non dire altro.

L'uso di C-Style getta qui è sbagliato:

QueueClass* p = /**/; 
IClassInterface* q = p; 

assert(((void*)p) == ((void*)q)); // may fire or not... 

Fondamentalmente non v'è alcuna garanzia che i 2 indirizzi sono uguali: si tratta di implementazione definita, ed è improbabile che resistere al cambiamento.

vi auguro di essere in grado di lanciare in modo sicuro il puntatore void* a un puntatore IClassInterface* allora avete bisogno di creare da una IClassInterface* in origine in modo che il compilatore C++ può eseguire il corretto l'aritmetica dei puntatori a seconda della disposizione degli oggetti.

Naturalmente, sottolineerò anche l'uso di variabili globali ... probabilmente lo sapete.

Per quanto riguarda la ragione dell'assenza? Onestamente non vedo nulla a parte un bug nel compilatore/linker. Ho visto la definizione inline delle funzioni virtual un paio di volte (in particolare, il metodo clone) e non ha mai causato problemi.

EDIT: Dal momento che "corretta l'aritmetica dei puntatori" non è stato così ben compreso, ecco un esempio

struct Base1 { char mDum1; }; 

struct Base2 { char mDum2; }; 

struct Derived: Base1, Base2 {}; 

int main(int argc, char* argv[]) 
{ 
    Derived d; 
    Base1* b1 = &d; 
    Base2* b2 = &d; 

    std::cout << "Base1: " << b1 
      << "\nBase2: " << b2 
      << "\nDerived: " << &d << std::endl; 

    return 0; 
} 

Ed ecco ciò che è stato stampato:

Base1: 0x7fbfffee60 
Base2: 0x7fbfffee61 
Derived: 0x7fbfffee60 

Non è la differenza tra il valore di b2 e &d, anche se si riferiscono a un'entità. Questo può essere compreso se si pensa al layout di memoria dell'oggetto.

Derived 
Base1  Base2 
+-------+-------+ 
| mDum1 | mDum2 | 
+-------+-------+ 

Quando la conversione da Derived* al Base2*, il compilatore eseguire le regolazioni necessarie (qui, incrementare l'indirizzo puntatore un byte) in modo che il puntatore finisce efficacemente indicando la Base2 parte di Derived e non al Base1 parte erroneamente interpretata come un oggetto Base2 (che sarebbe sgradevole).

Questo è il motivo per cui è necessario evitare l'uso di trasmissioni in stile C durante il downcasting. Qui, se hai un puntatore Base2 non puoi reinterpretarlo come puntatore Derived. Invece, sarà necessario utilizzare static_cast<Derived*>(b2) che decrementerà il puntatore di un byte in modo che punti correttamente all'inizio dell'oggetto Derived.

I puntatori di manipolazione vengono in genere definiti come aritmetica del puntatore. Qui il compilatore eseguirà automaticamente la regolazione corretta ... alla condizione di essere a conoscenza del tipo.

Sfortunatamente il compilatore non può eseguirli durante la conversione da un void*, è quindi compito dello sviluppatore assicurarsi che lo gestisca correttamente. La semplice regola empirica è la seguente: T* -> void* -> T* con lo stesso tipo che appare su entrambi i lati.

Pertanto, è necessario (semplicemente) correggere il codice dichiarando: IClassInterface* globalMember e non si avrebbe alcun problema di portabilità. Probabilmente avrai ancora problemi di manutenzione, ma questo è il problema dell'uso di C con codice OO: C non è a conoscenza di alcuna roba orientata agli oggetti in corso.

+0

Non capisco di cosa stai parlando! :) MA - 1) Posso solo avere void * nella funzione C esportata perché IT È la funzione esportata C! ;) 2) è la prima volta che sento che "il compilatore C++ può eseguire il corretto aritmetico del puntatore a seconda del layout degli oggetti" .. cosa ??:) –

+0

BTW - cosa c'è di sbagliato nel mio design? Beh, amo C++, ma le librerie condivise consentono solo le funzioni esportate da C. Cosa dovrei fare allora? Diventa coder C lame e rinunciare a tutto ciò che di buono C++ ofers? :) –

+0

Il problema è che si presume che un oggetto in C++ abbia un indirizzo univoco, qualunque sia il modo in cui ci si riferisce ad esso. Questo purtroppo non è il caso. Modificherò il mio post per aggiungere un piccolo esempio con i valori di Realworld per illustrarlo. –

Problemi correlati