2010-06-21 14 views
10

Ho letto alcune affermazioni vaghe che virtual inheritance non fornisce la struttura di memoria richiesta da COM, quindi dobbiamo utilizzare l'ereditarietà normale. L'ereditarietà virtuale è stata inventata per gestire il problema del diamante .Perché non possiamo usare "l'ereditarietà virtuale" in COM?

Qualcuno potrebbe mostrarmi un'illustrazione della differenza dei dettagli della struttura di memoria tra questi due approcci ereditari? E il motivo della chiave perché l'ereditarietà virtuale non è adatta per COM. Una foto sarebbe la migliore.

Molte grazie.

risposta

2

Le interfacce COM sono simili alle interfacce JAVA in un modo: non dispongono di membri dati. Ciò significa che l'ereditarietà dell'interfaccia è diversa dall'eredità di classe quando viene utilizzata l'ereditarietà multipla.

Per cominciare, ereditarietà considerare non virtuale con criteri di ereditarietà romboidali ...

  • B eredita A
  • C eredita un
  • D eredita B e C

Un'istanza di D contiene due istanze separate dei membri di dati di A. Ciò significa che quando un puntatore a A punta a un'istanza di D, deve identificare quale istanza di A all'interno di D significa - la punta r è diverso in ogni caso, e il cast del puntatore non sono semplici relabellings del tipo - anche l'indirizzo cambia.

Ora consideriamo lo stesso diamante con ereditarietà virtuale. Le istanze di B, C e D contengono tutte una singola istanza di A. Se si pensa che B e C abbiano un layout fisso (inclusa l'istanza A), questo è un problema. Se il layout Bs è [A, x] e il layout Cs è [A, y], quindi [B, C, z] non è valido per D - conterrebbe due istanze di A. Quello che devi usare è qualcosa come [ A, B ', C', z] dove B 'è tutto da B tranne l'ereditato A ecc.

Ciò significa che se si dispone di un puntatore a B, non si dispone di un unico schema per il dereferenziamento i membri ereditati da A. La ricerca di questi membri è diversa a seconda che il puntatore punti a un B puro o B all'interno di D o B all'interno di qualcos'altro. Il compilatore ha bisogno di alcuni indizi di runtime (tabelle virtuali) per trovare i membri ereditati da A. Finisci per aver bisogno di diversi puntatori a diverse tabelle virtuali nell'istanza di D, in quanto c'è un vtable per la B ereditata e per la C ereditata, il che implica un sovraccarico di memoria.

L'ereditarietà singola non presenta questi problemi. Il layout di memoria delle istanze viene mantenuto semplice e anche le tabelle virtuali sono più semplici. Ecco perché Java non consente l'ereditarietà multipla per le classi. Nell'ereditarietà dell'interfaccia non ci sono membri di dati, quindi di nuovo questi problemi semplicemente non si presentano - non c'è problema di quale-ereditato-A-con-D, né di diversi modi per trovare A-dentro-B a seconda di quale particolare B capita di essere dentro Sia COM che Java possono consentire l'ereditarietà multipla delle interfacce senza dover gestire queste complicazioni.

EDIT

ho dimenticato di dire - senza membri di dati, non v'è alcuna reale differenza tra l'ereditarietà virtuale e non virtuale. Tuttavia, con Visual C++, il layout è probabilmente diverso anche se non ci sono membri di dati, utilizzando sempre le stesse regole per ogni stile di ereditarietà indipendentemente dal fatto che siano presenti o meno membri di dati.

Inoltre, il layout di memoria COM corrisponde al layout di Visual-C++ (per i tipi di ereditarietà supportati) perché è stato progettato per farlo. Non c'è motivo per cui COM non possa essere stata progettata per supportare l'ereditarietà multipla e virtuale delle "interfacce" con i membri dei dati. Microsoft avrebbe potuto progettare la COM per supportare lo stesso modello di ereditarietà del C++, ma ha scelto di non farlo - e non c'è motivo per cui avrebbero dovuto fare diversamente.

Il codice COM precedente veniva spesso scritto in C, ovvero i layout di struttura scritti a mano che dovevano corrispondere esattamente al layout di Visual-C++ per funzionare. Layout per ereditarietà multipla e virtuale - beh, non mi offrirei volontario per farlo manualmente. Inoltre, COM era sempre la sua stessa cosa, pensata per collegare codice scritto in molte lingue diverse. Non è mai stato pensato per essere legato al C++.

ANCORA PIÙ DI EDITING

mi sono reso conto che ho perso un punto chiave.

In COM, l'unico problema di layout che interessa è la tabella virtuale, che deve gestire solo l'invio del metodo. Ci sono differenze significative nel layout a seconda che si prende l'approccio virtuale o non virtuale, simile al layout di su un oggetto con i membri di dati ...

  • Per non virtuale, il VTAB D contiene un A- dentro-B vtab e un vtab A-dentro-C.
  • Per virtuale, l'A si verifica solo una volta all'interno di Ds vtable, ma l'oggetto contiene più vtables e le ghiere del puntatore necessitano di modifiche dell'indirizzo.

Con interfaccia ereditarietà, questo è fondamentalmente dettaglio di implementazione - c'è solo un insieme di implementazioni dei metodi per A.

Nel caso non virtuale, le due copie della tabella virtuale Una sarebbero identici (portando allo stesso metodo di implementazione). È una tabella virtuale leggermente più grande, ma l'overhead per oggetto è inferiore e i cast del puntatore sono solo di tipo ribaltabile (nessun cambio di indirizzo). È un'implementazione più semplice e più efficiente.

COM non è in grado di rilevare il caso virtuale perché non c'è alcun indicatore nell'oggetto o vtable. Inoltre, non c'è motivo di supportare entrambe le convenzioni quando non ci sono membri dei dati. Supporta solo una semplice convenzione.

+1

Sono molto a disagio nel ricevere l'accettazione di questa risposta: come faccio a portarlo via? - Ho risposto pensando, modificato da continuazione di tutto ciò, e c'è qualcosa qui dentro che probabilmente spiega un aspetto chiave del perché questo è accaduto nei primi giorni della COM, ma ho solo una conoscenza molto limitata e datata della COM, e questo la risposta semplicemente non è accurata. In DCOM, semplicemente non può essere giusto, e anche nei giorni di back-end di OLE2, è un'implementazione di QueryInterface che probabilmente potrebbe fare tutto ciò che vuole in linea di principio. – Steve314

+0

"_ L'ereditarietà singola non presenta questi problemi._" Ovviamente, con il singolo c'è solo un vptr e un vtable condivisi. "Nell'ereditarietà dell'interfaccia non ci sono membri dati, quindi di nuovo questi problemi semplicemente non sorgono" se "questi problemi" si riferiscono alla tua precedente descrizione di "Finisci per aver bisogno di più puntatori a diverse tabelle virtuali nell'istanza D" e "implicando un sovraccarico di memoria "allora questo è ovviamente sbagliato: MI di interfacce implica il numero di vptr e vtables quante sono le interfacce ereditate, in C++ o in Java, se si desidera mantenere l'efficienza di runtime delle chiamate di funzione virtuale C++. – curiousguy

+0

"_Ho dimenticato di dire - senza membri di dati, non c'è alcuna differenza tra l'ereditarietà virtuale e non virtuale." "Assolutamente non corretto. Ho paura che semplicemente non capisci l'ereditarietà in C++. "Tuttavia, con Visual C++, il layout è probabilmente diverso anche se non ci sono membri dati" il layout non è certamente lo stesso per 'struct D: B' e per' struct D: virtual B'.Non vedo come potrebbero avere un layout simile, senza essere molto inefficiente. – curiousguy

4

Innanzitutto, in COM viene sempre utilizzato il comportamento dell'ereditarietà virtuale. QueryInterface non può restituire un valore diverso per es. il puntatore di base IUnknown in base a quale classe derivata è stata utilizzata per ottenerlo.

Ma hai ragione che non si tratta dello stesso meccanismo dell'eredità virtuale in C++. Il C++ non usa una funzione QueryInterface per upcast, quindi ha bisogno di un altro modo per ottenere il puntatore della classe base.

Il problema del layout di memoria è causato dal fatto che COM richiede che tutti i metodi di un'interfaccia di base possano essere richiamati direttamente utilizzando un puntatore a interfaccia derivata. AddRef è un buon esempio. In COM, è possibile chiamare AddRef e passare qualsiasi interfaccia derivata come puntatore this. In C++, l'implementazione AddRef si aspetta che questo puntatore sia di tipo IUnknown* const. La differenza è che in C++, il chiamante trova il puntatore di base, mentre in COM il callee esegue la regolazione per trovare il puntatore di base, quindi ogni interfaccia derivata richiede un'implementazione distinta (di almeno QueryInterface) consapevole dell'offset dal derivato puntatore all'interfaccia passato al puntatore di base.

A prima vista, un compilatore C++ potrebbe scegliere, come dettaglio di implementazione, che il callee esegua la regolazione proprio come COM. Ma le regole della funzione pointer-to-member non sono compatibili con questa implementazione delle classi base virtuali.

4

Una coclasse COM può implementare più interfacce ma ogni singola interfaccia deve implementare una v-table con puntatori a all dei metodi introdotti dalle sue interfacce di "base". Al minimo IUnknown. Se, ad esempio, implementa IPersistFile, deve fornire un'implementazione dei tre metodi IUnknown e IPersist :: GetClassID. E i metodi specifici di IPersistFile.

Che corrisponde al comportamento della maggior parte dei compilatori C++ quando implementano l'ereditarietà multipla non virtuale. Il compilatore imposta singole tabelle v per ogni classe ereditata (pure astratta). E lo riempie con i puntatori del metodo in modo che un metodo di classe comune implementi tutti i metodi che le interfacce hanno in comune. In altre parole, indipendentemente dal numero di interfacce implementate, vengono tutte servite da un metodo di classe come QueryInterface, AddRef o Release.

Esattamente come vorresti che funzionasse. Avere una implementazione di AddRef/Release semplifica il conteggio dei riferimenti per mantenere vivo l'oggetto della coclasse, indipendentemente da quanti diversi puntatori all'interfaccia si distribuiscono. QueryInterface è banale da implementare, un semplice cast fornisce il puntatore all'interfaccia a una v-table con il layout corretto.

L'ereditarietà virtuale non è richiesta. E molto probabilmente interromperà COM perché le tabelle v non hanno più il layout richiesto. Quale è difficile per qualsiasi compilatore, rivedere le opzioni/vm per il compilatore MSVC, ad esempio. Che COM così straordinariamente compatibile con il comportamento tipico di un compilatore C++ è non un incidente.

Btw, tutto questo colpisce il ventilatore quando una coclasse desidera implementare più interfacce che hanno un nome di metodo in comune che non è destinato a fare la stessa cosa. È una cosa abbastanza grossa e difficile da gestire. Menzionato in ATL Internals (DAdvise?), Ho purtroppo dimenticato la soluzione.

+0

+1. Vale anche la pena notare che i consumatori possono utilizzare solo il layout designato per l'interfaccia richiesta e niente di più. A volte può essere allettante imbrogliare, se si esegue un singolo appartamento e controllare sia l'oggetto COM che il consumatore. Tuttavia, le chiamate cross-apartment/DCOM non funzioneranno e falliranno in modo orribile. Le interfacce soggette al marshalling sono in realtà proxy al posto degli oggetti remoti, che quasi mai puntano all'oggetto provider originale. – meklarian

Problemi correlati