Warum genau muss ich eine explizite Verallgemeinerung bei der Implementierung von QueryInterface() in ein Objekt mit mehreren Schnittstellen()
Angenommen ich habe eine Klasse die zwei oder mehr COM-Schnittstellen:
class CMyClass : public IInterface1, public IInterface2 {
};
Fast jedes Dokument, das ich sah, deutet darauf hin, dass, wenn ich die Implementierung von QueryInterface() für IUnknown ich ausdrücklich Verallgemeinerung diese Zeiger auf eine der Schnittstellen:
if( iid == __uuidof( IUnknown ) ) {
*ppv = static_cast<IInterface1>( this );
//call Addref(), return S_OK
}
Die Frage ist, warum kann ich nicht einfach kopieren diese?
if( iid == __uuidof( IUnknown ) ) {
*ppv = this;
//call Addref(), return S_OK
}
Die Dokumente in der Regel sagen, dass, wenn ich letzteres tun, werde ich gegen die Forderung, dass jeder Aufruf von QueryInterface() auf das gleiche Objekt muss wieder genau den gleichen Wert.
Ich nicht ganz verstehe. Bedeuten Sie, dass, wenn ich QI() für IInterface2 und rufen Sie QueryInterface() durch, der Zeiger C++ passieren diese etwas anders aus, wenn ich QI() für IInterface2, da C++ wird jedes mal machen diese Punkt, um ein Unterobjekt?
- Wie ist
ppv
definiert? - void**, wie üblich in der QueryInterface () - Proben.
- So gilt: C* c = static_cast< C* >(das); garantieren einen Zeiger auf das richtige sub-Klasse? Oder muss ich dynamic_cast?
- static_cast ist genug, wenn Sie Stimmen, um eine Basis-Klasse, der compiler hat genug Daten, um die notwendigen Anpassungen während der Kompilierung. Der compiler wird sich weigern zu tun, die static_cast einer abgeleiteten Klasse, aber ein dynamic_cast noch erfolgreich sein könnte.
Du musst angemeldet sein, um einen Kommentar abzugeben.
Das problem ist, dass
*ppv
ist in der Regel einevoid*
- direkt zuweisenthis
es wird einfach die vorhandenethis
Zeiger und geben*ppv
den Wert der it (da alle Zeiger umgewandelt werden kannvoid*
).Dies ist nicht ein problem mit einfachvererbung, weil mit einfacher Vererbung die Basisklasse Zeiger ist immer das gleiche für alle Klassen (da die vtable wird nur verlängert für die abgeleiteten Klassen).
Jedoch - für Mehrfachvererbung, die Sie tatsächlich am Ende mit mehreren base-Pointer, je nachdem, welche 'view' der Klasse, die Sie reden! Der Grund dafür ist, dass mit Mehrfachvererbung können Sie nicht nur die Erweiterung der vtable - Sie benötigen mehrere vtables je nachdem, in welcher Branche du redest.
So müssen Sie zum cast der
this
Zeiger, um sicherzustellen, dass der compiler legt die richtige base pointer (für die richtige vtable) in*ppv
.Hier ist ein Beispiel für einfachvererbung:
vtable für Eine:
vtable für B:
Beachten Sie, dass wenn Sie die
B
vtable und Sie behandeln Sie wie eineA
vtable-es funktioniert einfach - die offsets für die Mitglieder derA
sind genau das, was Sie erwarten würde.Hier ist ein Beispiel für die Verwendung von Mehrfachvererbung (mit Definitionen von
A
undB
von oben) (Hinweis: nur ein Beispiel - Implementierungen können variieren):vtable für C:
vtable für D:
Und die tatsächliche Speicher-layout für
D
:Beachten Sie, dass bei der Behandlung einer
D
vtable alsA
es funktionieren wird (das ist Zufall - man kann sich auf Sie nicht verlassen). Aber - wenn Sie der Behandlung einerD
vtable alsC
beim Aufrufc0
(die der compiler erwartet in Steckplatz 0 des vtable), wirst du plötzlich rufta0
!Beim Aufruf
c0
auf eineD
was der compiler tut, ist es tatsächlich geht ein fakethis
Zeiger, die hat eine vtable, die so aussieht, wie es sollte für eineC
.Also, wenn Sie rufen eine
C
Funktion aufD
es muss passen Sie die vtable zeigen Sie auf die Mitte desD
Objekt (an der@C
vtable), bevor Sie die Funktion aufrufen.void**
es gibt keine compiler erzwungene Typsicherheit (oder implizite Typumwandlung).@a0 A B @c0 C D
wo A, B, C und D, die Verwirklichung der tatsächlichen Parametern jeder Klasse. Ich würde schätzen, wenn Sie hatte mehr detaillierte Erläuterung der tatsächliche Speicher-layout (für einen bestimmten compiler) bei der MI beteiligt ist.D
Instanz. Ich aktualisiert die Beschreibung ein wenig, um ein Beispiel Speicher-layout fürD
. Ein whiteboard wäre viel besser für diese... 🙂Du tust, COM-Programmierung, so gibt es ein paar Dinge zu erinnern, über Ihren code, bevor wir uns mit warum
QueryInterface
implementiert ist, wie es ist.IInterface1
undIInterface2
Abstieg ausIUnknown
, und nehmen wir an, weder ist ein Abkömmling des anderen.QueryInterface(IID_IUnknown, (void**)&intf)
auf Ihr Objektintf
erklärt wird, als ArtIUnknown*
.QueryInterface
genannt werden könnte durch eine von Ihnen.Da Nummer #3, der Wert von
this
in IhremQueryInterface
definition variieren kann. Rufen Sie die Funktion über eineIInterface1
Zeiger, undthis
haben einen anderen Wert, als es wäre, wenn es waren aufgerufen, über eineIInterface2
Zeiger. In jedem Fallthis
halten wird ein Gültiger Zeiger vom TypIUnknown*
aufgrund von Punkt #1, so dass, wenn Sie teilen einfach*ppv = this
Anrufer glücklich, aus einem C++ - Sicht. Sie haben gespeichert, die einen Wert vom TypIUnknown*
in eine variable des gleichen Typs (siehe Punkt #2), also alles in Ordnung.Jedoch COM hat stärkere Regeln als gewöhnliche C++. Insbesondere erfordert, dass jede Anforderung für die
IUnknown
Schnittstelle eines Objekts zurückgeben müssen den gleichen Zeiger, egal mit welchem "Blick" des Objekts verwendet wurde, zum aufrufen der Abfrage. Es ist also nicht ausreichend für Ihr Objekt zu zuweisen, bloßethis
in*ppv
. Manchmal werden Anrufer bekommen würde, dieIInterface1
version, und manchmal, Sie würden Holen Sie sich dieIInterface2
version. Einen richtigen COM-Implementierung muss sicherstellen, dass es gibt konsistente Ergebnisse. Es wird Häufig eineif
-else
Leiter Prüfung für alle unterstützten interfaces, aber eine der Bedingungen zu überprüfen, werden zwei Schnittstellen statt nur einer, die zweiteIUnknown
:Ist es egal, welche Schnittstelle die
IUnknown
überprüfen gruppiert mit, solange die Gruppierung nicht ändern, während das Objekt existiert noch immer, aber Sie würde wirklich gehen aus dem Weg zu schaffen.