Ich habe von hier über den Adjustor Thunk gelesen. Hier ist ein Zitat:
Nun gibt es nur ein QueryInterface Methode, aber es gibt zwei Einträge, einen für jede vtable. Denken Sie daran, dass jeder Funktion in einer Vtable empfängt die entsprechender Schnittstellenzeiger als sein "dieser" Parameter Das ist gut für QueryInterface (1); seine Schnittstelle Zeiger ist der gleiche wie der des Objekts Schnittstellenzeiger. Aber das sind schlechte Nachrichten für QueryInterface (2), seit seiner Schnittstellenzeiger ist q, nicht p.
Hier kommen die Einsteller-Thunks in.
Ich frage mich, warum " jede Funktion in einer vtable den entsprechenden Schnittstellenzeiger als seinen" this "Parameter erhält "? Ist es der einzige Hinweis (Basisadresse), der von der Schnittstellenmethode verwendet wird, um Datenelemente in der Objektinstanz zu finden?
Hier ist mein letztes Verständnis:
Tatsächlich geht es bei meiner Frage nicht um den Zweck des Parameters this , sondern darum, warum wir den entsprechenden Schnittstellenzeiger als verwenden müssen Dieser Parameter. Entschuldige meine Ungenauigkeit.
Neben der Verwendung des Schnittstellenzeigers als Locator / Halt im Layout eines Objekts. Es gibt natürlich andere Mittel, um dies zu tun, solange Sie der Implementierer der Komponente sind.
Aber das ist nicht der Fall für die Kunden unserer Komponente.
Wenn die Komponente COM-basiert ist, wissen Clients unserer Komponente nichts über die Interna unserer Komponente. Clients können nur den Schnittstellenzeiger übernehmen, und dies ist genau der Zeiger, der als this -Parameter an die Schnittstellenmethode übergeben wird. Unter dieser Erwartung hat der Compiler keine andere Wahl als den Code der Schnittstellenmethode basierend auf diesem spezifischen this -Zeiger zu generieren .
Also führt das obige Argument zu dem Ergebnis, dass:
Es muss sichergestellt werden, dass jede Funktion in einer vtable muss empfangen werden entsprechender Schnittstellenzeiger als sein " dieser " -Parameter.
Im Fall von "This pointer adjector thunk" gibt es zwei verschiedene Einträge für eine einzelne QueryInterface () Methode, mit anderen Worten, zwei verschiedene Interface Pointer können verwendet werden, um die QueryInterface () Methode aufzurufen, aber der Compiler erzeugt nur 1 Kopie der Methode QueryInterface (). Wenn also eine der Schnittstellen vom Compiler als This-Pointer gewählt wird, müssen wir die andere auf die gewählte einstellen. Dies ist, wofür dieser Thunder der Anpassung herkommt.
BTW-1, was ist, wenn der Compiler zwei verschiedene Instanzen der Methode QueryInterface () generieren kann? Jeder basiert auf dem entsprechenden Schnittstellenzeiger. Dies erfordert nicht das Einstell-Thunk, aber es würde mehr Platz benötigen, um den zusätzlichen, aber ähnlichen Code zu speichern.
BTW-2: Es scheint, dass manchmal eine Frage aus Sicht des Implementierers keine vernünftige Erklärung hat, sondern aus dem Blickwinkel des Benutzers besser verstanden werden kann.
Wenn Sie den COM-Teil von der Frage entfernen, ist this
pointer adjector thunk ein Stück Code, der dafür sorgt, dass jede Funktion einen this
-Zeiger erhält, der auf das Unterobjekt des konkreten Typs zeigt. Das Problem tritt mit Mehrfachvererbung auf, wobei die Basis- und abgeleiteten Objekte nicht ausgerichtet sind.
Betrachten Sie den folgenden Code:
%Vor% (Und ignorieren Sie die fehlende Initialisierung). Das Unterobjekt base
in derived
ist nicht auf den Anfang des Objekts ausgerichtet, da sich zwischen [1] ein offset
befindet. Wenn ein Zeiger auf derived
auf einen Zeiger auf base
geworfen wird (einschließlich impliziter Umwandlungen, aber keine Umwandlungen von Umwandlungen, die zu UB und potentiellem Tod führen würden), wird der Wert des Zeigers versetzt, sodass (void*)d != (void*)((base*)d)
für ein angenommenes Objekt d
des Typs derived
.
Berücksichtigen Sie jetzt die Verwendung:
%Vor% Das Problem tritt auf, wenn eine Funktion von einem base
Zeiger oder Verweis aufgerufen wird. Wenn der virtuelle Dispatch-Mechanismus feststellt, dass der letzte Overrider in base
ist, muss sich der Pointer this
auf das base
-Objekt beziehen, wie in b->bar
, wobei der implizite this
Pointer dieselbe Adresse ist %Code%. Wenn nun der letzte Overrider in einer abgeleiteten Klasse ist, wie bei b
, muss der b->foo()
-Zeiger auf den Anfang des Unterobjekts des Typs ausgerichtet werden, in dem der finale Overrider gefunden wird (in diesem Fall this
).
Was der Compiler macht, ist ein Zwischenstück Code zu erstellen. Wenn der virtuelle Dispatch-Mechanismus aufgerufen wird und vor dem Senden an derived
, übernimmt der Zwischenaufruf den derived::foo
-Zeiger und subtrahiert den Offset auf den Anfang des this
-Objekts. Dieser Vorgang ist identisch mit einem Downcast derived
. Beachten Sie, dass der Zeiger static_cast<derived*>(this)
an diesem Punkt vom Typ this
ist, sodass er anfänglich versetzt wurde und dadurch der ursprüngliche Wert base
zurückgegeben wird.
[1] Es gibt einen Offset auch bei Schnittstellen - im Sinne von Java / C #: Klassen, die nur virtuelle Methoden definieren - wie Sie müssen eine Tabelle an die Vtable dieser Schnittstelle speichern.
Hier ist ein Artikel über MSVC-Interna von einem der Designer. Es erklärt dies und viele andere Details der MSVC-Implementierung. Vielleicht möchten Sie auch meinen Artikel über OpenRCE darüber lesen, wie alles in Assembly aussieht.
Ja, this
ist wichtig für das Auffinden des Startpunkts des Objekts. Sie schreiben in Ihren Code:
wobei variable
die Membervariable ist. Zuallererst, zu welchem Objekt gehört es? Es gehört zu dem Objekt, auf das der Zeiger this
zeigt. So ist es eigentlich
Jetzt muss C ++ Code erzeugen, der den Job erledigt - Daten kopieren. Um dies zu tun, muss der Abstand zwischen dem Objektstart und der Elementvariablen bekannt sein. Die Konvention ist, dass this
immer auf den Objektstart zeigt, also kann der Offset konstant sein:
Ich denke, dass es wichtig ist, darauf hinzuweisen, dass es in C ++ keine solche Entität wie "Schnittstellenzeiger" oder etwas ähnliches gibt. Es ist ein Idiom bestenfalls auf dem Konzept einer eingeschränkten abstrakten Klasse aufgebaut, aber immer noch eine Klasse. Als solche gelten alle für die Klassenmitglieder geltenden Regeln und die Handhabung von "dieser" unverändert. Grundsätzlich muss sich eine Interface-Klasse unabhängig von ihrer Funktion und eventuellen Vererbungshierarchie wie eigenständige Klassen eines gegebenen Typs verhalten.
Wir können den Aufrufmechanismus der virtuellen Methode verwenden, um zum tatsächlichen (dynamischen Typ) des Objekts zu gelangen, das von einer (Schnittstellen-) Basisklasse offengelegt wird. Wie es gemacht wird, ist die Implementierung spezifisch mit solchen Konzepten wie der virtuellen Methodentabelle und "Adjustor Thunks". Im Allgemeinen kann der Compiler seinen anfänglichen "this" -Zeiger verwenden, um die VMT und dann die tatsächliche Implementierung einer gegebenen Funktion zu lokalisieren und sie mit einer eventuellen Anpassung des "this" -Zeigers aufzurufen. Die Thunk-Anpassung wird im Allgemeinen benötigt, um den letzten Aufruf auszuführen, wenn das Speicherlayout der Basisklasse anders ist als ein abgeleiteter, auf den wir im Falle der Mehrfachvererbung einen Bezug haben.