2009-07-06 8 views
22

Betrachten Sie den folgenden C++ Code:Zeiger auf virtuelle Elementfunktionen. Wie funktioniert es?

class A 
{ 
public: 
     virtual void f()=0; 
}; 


int main() 
{ 
    void (A::*f)()=&A::f; 
} 

Wenn ich denke, müsste, würde ich sagen, dass & A :: f in diesem Zusammenhang "die Adresse der A-Implementierung von f()" bedeuten würde, da es keine explizite Trennung zwischen Zeigern zu regulären Mitgliedsfunktionen und virtuellen Mitgliedsfunktionen gibt. Und da A f() nicht implementiert, wäre das ein Kompilierungsfehler. Es ist jedoch nicht.

Und nicht nur das. Der folgende Code:

void (A::*f)()=&A::f; 
A *a=new B;   // B is a subclass of A, which implements f() 
(a->*f)(); 

wird tatsächlich B :: f aufrufen.

Wie passiert das?

+0

Weil der Compiler es möglich macht! Wenn das Aufrufen einer normalen Methode nicht anders ist als das Aufrufen einer virtuellen Methode, warum ist der Code bei der Verwendung von Methodenzeigern anders? Was denkst du, übersetzt der Compiler normale Methoden (virtuelle und ordingliche) Aufrufe? –

Antwort

9

Hier sind viel zu viele Informationen über Mitgliedsfunktionszeiger. Es gibt einige Sachen über virtuelle Funktionen unter "Die wohlbehüteten Compiler", obwohl IIRC, als ich den Artikel las, den ich diesen Teil abgeschöpft habe, da der Artikel tatsächlich über die Implementierung von Delegaten in C++ geht.

http://www.codeproject.com/KB/cpp/FastDelegate.aspx

Die kurze Antwort ist, dass es auf dem Compiler abhängig ist, aber eine Möglichkeit ist, dass das Mitglied Funktionszeiger als Struktur einen Zeiger auf eine „Thunk“ Funktion implementiert ist enthalten, die den virtuellen Anruf tätigt.

+0

hi, du hast gerade auf thunk hingewiesen. Gibt es einen guten Artikel, der thunk erklärt? – anand

+0

"Das Wort Thunk bezieht sich auf ein Stück Low-Level-Code, meist maschinell erzeugt, der einige Details eines bestimmten Softwaresystems implementiert.", Von http://en.wikipedia.org/wiki/Thunk. Auf dieser Seite werden verschiedene Arten von Thunks besprochen, wenn auch nicht diese. –

22

Es funktioniert, weil der Standard sagt, so soll es geschehen. Ich habe einige Tests mit GCC durchgeführt, und es stellt sich heraus, dass für virtuelle Funktionen GCC den virtuellen Tabellen-Offset der fraglichen Funktion in Bytes speichert.

struct A { virtual void f() { } virtual void g() { } }; 
int main() { 
    union insp { 
    void (A::*pf)(); 
    ptrdiff_t pd[2]; 
    }; 
    insp p[] = { { &A::f }, { &A::g } }; 
    std::cout << p[0].pd[0] << " " 
      << p[1].pd[0] << std::endl; 
} 

Das Programm gibt 1 5 - die Byte-Offsets der virtuellen Tabelleneinträge dieser beiden Funktionen. Es folgt die Itanium C++ ABI, which specifies that.

+0

Ich nehme an, dass die Antwort auf meine Frage nicht C++ standardisiert ist. Dasselbe gilt für Vtables, aber ich kenne keinen Compiler, der nicht Vtables als Mechanismus für virtuelle Funktionen verwendet, also nehme ich an, dass es auch einen * Standard * -Mechanismus gibt. Deine Antwort macht mich nur verwirrter.Wenn der Compiler die Zeiger 1 und 5 in Zeigern auf die Member-Funktionen von A speichert, wie kann er dann feststellen, ob das ein V-Tabellen-Index oder eine echte Adresse ist? (Beachten Sie, dass es keinen Unterschied zwischen Zeigern zu regulären und virtuellen Elementfunktionen gibt) –

+0

Warum macht diese Antwort Sie verwirrter? Frag einfach, ob etwas unklar ist. Diese Art von Dingen ist nicht standardisiert. Es liegt an der Implementierung, über Möglichkeiten nachzudenken, um es zu lösen. Sie können entscheiden, ob es ein Funktionszeiger ist oder nicht: Ich denke, das ist der Grund, warum sie 1 addieren. Wenn also die Zahl nicht ausgerichtet ist, handelt es sich um einen Vtable-Offset. Wenn es ausgerichtet ist, ist es ein Zeiger auf eine Elementfunktion. Das ist nur eine Vermutung von mir. –

+0

Danke, das klingt logisch. Allerdings etwas uneffizient für eine C++ - Implementierung ... Überprüft den Code auf VC, und die Ergebnisse sind völlig unterschiedlich. Die Ausgabe ist "c01380 c01390", was wie eine Adresse von etwas aussieht. –

1

Ich bin nicht ganz sicher, aber ich denke, es ist nur regelmäßige polymorphe Verhalten. Ich denke, dass &A::f eigentlich die Adresse des Funktionszeigers in der Vtable der Klasse bedeutet, und deshalb erhalten Sie keinen Compilerfehler. Der Speicherplatz in der VTable ist immer noch zugewiesen und das ist der Ort, den Sie tatsächlich zurückbekommen.

Dies ist sinnvoll, da abgeleitete Klassen diese Werte im Wesentlichen mit Zeigern auf ihre Funktionen überschreiben. Dies ist der Grund, warum (a->*f)() in Ihrem zweiten Beispiel funktioniert - f referenziert die vtable, die in der abgeleiteten Klasse implementiert ist.

+1

Das könnte der Fall gewesen sein, wenn es eine Trennung zwischen Zeigern zu regulären Memberfunktionen und Zeigern zu virtuellen Memberfunktionen gab. Wie ich bereits erwähnt habe, gibt es das nicht, und darum geht es. –

+0

Ein Compiler ist definitiv erlaubt, alle Methoden, virtuell oder nicht, in die vtable zu stellen. Wenn dies der Fall ist, kann es den vtable-Index für Zeiger auf Elementfunktionen verwenden. Ziemlich einfach für den Compiler tatsächlich - stellen Sie einfach sicher, dass ein nicht-virtueller Overrider seinen eigenen vtable-Eintrag erhält, anstatt den Basisklasseneintrag zu überschreiben. – MSalters