Perché esattamente ho bisogno di un upcast esplicito quando si implementa QueryInterface () in un object con più interfacce ()

Supponiamo che abbia una class che implementa due o più interfacce COM:

class CMyClass : public IInterface1, public IInterface2 { }; 

Quasi tutti i documenti che ho visto suggeriscono che quando implemento QueryInterface () per IUnknown ho esplicitamente trasmesso questo puntatore a una delle interfacce:

 if( iid == __uuidof( IUnknown ) ) { *ppv = static_cast( this ); //call Addref(), return S_OK } 

La domanda è: perché non posso semplicemente copiare questo ?

 if( iid == __uuidof( IUnknown ) ) { *ppv = this; //call Addref(), return S_OK } 

I documenti di solito dicono che se faccio il secondo violerò il requisito che ogni chiamata a QueryInterface () sullo stesso object debba restituire esattamente lo stesso valore.

Non lo capisco. Significano che se I QI () per IInterface2 e chiamare QueryInterface () attraverso quel puntatore C ++ passerà questo leggermente diverso da if I QI () per IInterface2 perché C ++ farà ogni volta questo punto a un subobject?

Il problema è che *ppv solito è un void* – assegnando direttamente this ad esso basta prendere il puntatore esistente e dare *ppv al valore di esso (poiché tutti i puntatori possono essere lanciati su void* ).

Questo non è un problema con l’ereditarietà singola perché con l’ereditarietà singola il puntatore di base è sempre lo stesso per tutte le classi (perché il vtable è appena esteso per le classi derivate).

Tuttavia, per l’ereditarietà multipla hai effettivamente più puntatori di base, a seconda di quale “vista” della class di cui stai parlando! La ragione di ciò è che con l’ereditarietà multipla non puoi semplicemente estendere il vtable – hai bisogno di più vtables a seconda del ramo di cui stai parlando.

Quindi devi lanciare this puntatore per assicurarti che il compilatore inserisca il puntatore di base corretto (per il vtable corretto) in *ppv .

Ecco un esempio di ereditarietà singola:

 class A { virtual void fa0(); virtual void fa1(); int a0; }; class B : public A { virtual void fb0(); virtual void fb1(); int b0; }; 

vtable per A:

 [0] fa0 [1] fa1 

vtable per B:

 [0] fa0 [1] fa1 [2] fb0 [3] fb1 

Nota che se hai il B vtable e lo tratti come un A vtable funziona semplicemente – gli offset per i membri di A sono esattamente ciò che ti aspetteresti.

Ecco un esempio che utilizza l’ereditarietà multipla (utilizzando le definizioni di A e B dall’alto) (nota: solo un esempio – le implementazioni possono variare):

 class C { virtual void fc0(); virtual void fc1(); int c0; }; class D : public B, public C { virtual void fd0(); virtual void fd1(); int d0; }; 

vtable per C:

 [0] fc0 [1] fc1 

vtable per D:

 @A: [0] fa0 [1] fa1 [2] fb0 [3] fb1 [4] fd0 [5] fd1 @C: [0] fc0 [1] fc1 [2] fd0 [3] fd1 

E il layout della memoria reale per D :

 [0] @A vtable [1] a0 [2] b0 [3] @C vtable [4] c0 [5] d0 

Si noti che se si considera un D vtable come un A funzionerà (questa è una coincidenza – non si può fare affidamento su di esso). Tuttavia, se si considera un D vtable come C quando si chiama c0 (che il compilatore si aspetta nello slot 0 del vtable) si chiamerà improvvisamente a0 !

Quando si chiama c0 su una D ciò che fa il compilatore in realtà passa un falso this puntatore che ha un vtable che sembra come dovrebbe per un C

Quindi, quando si chiama una funzione C su D è necessario regolare il vtable in modo che punti al centro dell’object D (al vtable @C ) prima di chiamare la funzione.

Stai facendo programmazione COM, quindi ci sono alcune cose da ricordare sul tuo codice prima di guardare perché QueryInterface è implementato così com’è.

  1. Sia IInterface1 che IInterface2 discendono da IUnknown e assumiamo che nessuno dei due sia discendente dell’altro.
  2. Quando qualcosa chiama QueryInterface(IID_IUnknown, (void**)&intf) sul tuo object, intf sarà dichiarato come tipo IUnknown* .
  3. Ci sono più “viste” del tuo object – puntatori di interfaccia – e QueryInterface può essere chiamato attraverso uno qualsiasi di essi.

Poiché il punto 3, il valore di this nella definizione di QueryInterface può variare. Chiamare la funzione tramite un puntatore IInterface1 , e this avrà un valore diverso da quello che sarebbe se fosse chiamato tramite un puntatore IInterface2 . In entrambi i casi, this terrà un puntatore valido di tipo IUnknown* causa del punto # 1, quindi se si assegna semplicemente *ppv = this , il chiamante sarà felice, da un punto di vista C ++ . Avrai memorizzato un valore di tipo IUnknown* in una variabile dello stesso tipo (vedi punto 2), quindi tutto va bene.

Tuttavia, COM ha regole più forti rispetto al normale C ++ . In particolare, richiede che qualsiasi richiesta per l’interfaccia IUnknown di un object debba restituire lo stesso puntatore, indipendentemente dalla “vista” di tale object utilizzata per richiamare la query. Pertanto, non è sufficiente che il tuo object assegni sempre this in *ppv . A volte i chiamanti otterrebbero la versione IInterface1 , ea volte otterrebbero la versione IInterface2 . Un’implementazione COM appropriata deve assicurarsi che restituisca risultati coerenti. Normalmente avrà una ladder if else che controlla tutte le interfacce supportate, ma una delle condizioni controllerà due interfacce invece di una sola, mentre la seconda è IUnknown :

 if (iid == IID_IUnknown || iid == IID_IInterface1) { *ppv = static_cast(this); } else if (iid == IID_IInterface2) { *ppv = static_cast(this); } else { *ppv = NULL; return E_NOINTERFACE; } AddRef(); return S_OK; 

Non importa quale interfaccia il controllo IUnknown è raggruppato fino a quando il raggruppamento non cambia mentre l’object esiste ancora, ma dovresti davvero fare di tutto per farlo accadere.