2009-11-16 13 views
14

Si supponga ho una classe che implementa due o più interfacce COM:Perché esattamente ho bisogno di un upcast esplicito in sede di attuazione QueryInterface() in un oggetto con interfacce multiple()

class CMyClass : public IInterface1, public IInterface2 { 
}; 

Quasi tutti i documenti che ho visto suggerisce che quando a implementare QueryInterface() per IUnknown ho esplicitamente upcast questo puntatore ad una delle interfacce:

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

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

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

I documenti di solito dicono che se faccio il secondo io violare il requisito che qualsiasi chiamata a QueryInterface() sullo stesso oggetto deve restituire esattamente lo stesso valore.

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

+0

Come viene definito 'ppv'? – spoulson

+1

vuoto come di consueto negli esempi di QueryInterface(). – sharptooth

+0

Così fa: C * c = static_cast < C* > (questo); garantire un puntatore alla sottoclasse corretta? O devo usare dynamic_cast? –

risposta

26

Il problema è che *ppv è di solito un void* - assegnando direttamente this ad esso si limiterà a prendere la this puntatore esistente e dare *ppv il valore di esso (in quanto tutti i puntatori possono essere lanciati a 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 si finisce con più puntatori di base, a seconda di quale "vista" della classe di cui si sta 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 è necessario eseguire il cast del puntatore this per assicurarsi 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 la B:

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

Si noti che se avete la B vtable voi e trattalo come un A vtable funziona solo - gli scostamenti per i membri di A sono esattamente ciò che ti aspetteresti.

Ecco un esempio utilizzando 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 l'attuale m Layout Emory per D:

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

Si noti che se si trattano un D vtable come A funzionerà (questo è un caso - non si può fare affidamento su di esso). Tuttavia, se trattate un vtable come C quando chiamate il numero c0 (che il compilatore si aspetta nello slot 0 del vtable) chiamerete improvvisamente a0!

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

Così, quando si chiama una funzione C su D si ha la necessità di regolare il vtable per puntare al centro dell'oggetto D (al @C vtable) prima di chiamare la funzione.

+0

+1 Grande spiegazione – Patrick

+0

"a seconda di quale vista" si riduce a "quale tipo di puntatore viene utilizzato per chiamare QueryInterface()"? – sharptooth

+1

Fondamentalmente sì - il chiamante si aspetta un oggetto che abbia un layout vtable esattamente come quello che si aspetta. Comunque, dato che stanno passando un 'void **' non c'è alcun tipo di sicurezza del compilatore (o implicita). – Aaron

7

Stai facendo la programmazione COM, quindi ci sono alcune cose da ricordare sul tuo codice prima di guardare al motivo per cui lo QueryInterface è implementato così com'è.

  1. Sia IInterface1 e IInterface2 discendono da IUnknown, e supponiamo che nessuno dei due è un discendente dell'altro.
  2. Quando qualcosa chiama QueryInterface(IID_IUnknown, (void**)&intf) sul proprio oggetto, intf verrà dichiarato come tipo IUnknown*.
  3. Ci sono più "viste" del tuo oggetto - puntatori di interfaccia - e QueryInterface potrebbe essere chiamato attraverso uno qualsiasi di questi.

Poiché punto # 3, il valore di this nella vostra QueryInterface definizione 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 manterrà un puntatore valido di tipo IUnknown* a 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ù potenti rispetto al normale C++. In particolare, richiede che qualsiasi richiesta per l'interfaccia IUnknown di un oggetto debba restituire lo stesso puntatore, indipendentemente dalla "vista" di tale oggetto utilizzata per richiamare la query.Pertanto, non è sufficiente che l'oggetto assegni sempre il this allo *ppv. A volte i chiamanti ottengono la versione IInterface1 e talvolta ricevono la versione IInterface2. Un'implementazione COM appropriata deve assicurarsi che restituisca risultati coerenti. Sarà comunemente avere un if - scaletta else controllo per tutte le interfacce supportate, ma una delle condizioni controllerà per due interfacce invece di uno, il secondo essendo IUnknown:

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

Non importa quale interfaccia il Il controllo IUnknown viene raggruppato fino a quando il raggruppamento non cambia mentre l'oggetto esiste ancora, ma dovresti davvero fare di tutto per farlo.

Problemi correlati