2009-07-06 18 views
22

Rozważmy następujący kod C++:Wskaźniki do funkcji wirtualnego członka. Jak to działa?

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


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

Gdybym miał zgadywać, powiedziałbym, że & A :: f w tym kontekście oznaczałoby „adres wdrażania A w f()”, ponieważ nie ma wyraźnej separacji między wskaźnikami do zwykłych funkcji składowych i funkcji wirtualnego elementu. A ponieważ A nie implementuje f(), byłby to błąd kompilacji. Jednak tak nie jest.

I nie tylko to. Poniższy kod:

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

będzie faktycznie wywoływać B :: f.

Jak to się dzieje?

+0

Ponieważ kompilator tak się dzieje! Jeśli wywołanie zwykłej metody nie różni się od wywoływania metody wirtualnej, dlaczego uważasz, że kod jest inny, gdy używasz wskaźników metod? Jak sądzisz, w jaki sposób kompilator tłumaczy normalne wywołania metody (wirtualne i wywołujące)? –

Odpowiedz

9

Oto zbyt wiele informacji na temat wskaźników funkcji członka. Jest trochę rzeczy o funkcjach wirtualnych w "Kompilatorach dobrze zachowujących się", chociaż IIRC, kiedy czytałem artykuł, który przeglądałem tę część, ponieważ artykuł w rzeczywistości dotyczy wdrażania delegatów w C++.

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

Krótka odpowiedź jest taka, że ​​to zależy od kompilatora, ale jedną z możliwości jest to, że wskaźnik funkcji członka jest zaimplementowany jako struct zawierającego wskaźnik do funkcji „pomyślał” co sprawia, że ​​wirtualne połączenie.

+0

Cześć, właśnie wskazałeś na thunk. Czy jest tam jakiś dobry artykuł wyjaśniający thunk? – anand

+0

"słowo thunk odnosi się do kodu niskiego poziomu, zwykle generowanego maszynowo, który implementuje pewne szczegóły konkretnego oprogramowania", z http://en.wikipedia.org/wiki/Thunk. Ta strona omawia kilka rodzajów dźwięków, choć nie tego konkretnego. –

22

Działa, ponieważ Standard mówi, że tak powinno się stać. Zrobiłem kilka testów z GCC i okazało się, że dla funkcji wirtualnych, GCC przechowuje przesunięcie tablicy wirtualnej dla danej funkcji, w bajtach.

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; 
} 

To wyjścia programu 1 5 - przesunięcia bajt wirtualnych wpisów tabeli tych dwóch funkcji. Wynika z tego, że Itanium C++ ABI .

+0

Przypuszczam, że odpowiedź na moje pytanie nie jest standaryzowana w C++. Jednakże są to również vtable, ale nie znam żadnego kompilatora, który nie używa vtables jako mechanizmu dla funkcji wirtualnych, więc przypuszczam, że istnieje również mechanizm * standard *. Twoja odpowiedź tylko sprawia, że ​​jestem bardziej zdezorientowany.Jeśli kompilator przechowuje wartości 1 i 5 w wskaźnikach funkcji składowych A, to w jaki sposób może stwierdzić, czy jest to indeks vtable czy prawdziwy adres? (zauważ, że nie ma różnicy między wskaźnikami do regularnych i wirtualnych funkcji członków). –

+0

Dlaczego ta odpowiedź powoduje większe zamieszanie? Po prostu zapytaj, czy tam jest coś niejasnego. Tego rodzaju rzeczy nie są ustandaryzowane. Od implementacji zależy sposób jej rozwiązania. Mogą zdecydować, czy jest to wskaźnik funkcji, czy nie: myślę, że dlatego dodają 1. Tak więc, jeśli liczba nie jest wyrównana, jest to offset wyrównawczy. Jeśli jest wyrównany, jest wskaźnikiem do funkcji składowej. Ale to tylko odgadnięcie. –

+0

Dzięki, to brzmi logicznie. Jednak nieco nieskuteczne dla implementacji C++ ... Sprawdziłem kod na VC, a wyniki są zupełnie inne. Wyjście to "c01380 c01390", które wydaje się być adresem czegoś. –

1

Nie jestem do końca pewien, ale myślę, że to zwykłe polimorficzne zachowanie. Myślę, że &A::f w rzeczywistości oznacza adres wskaźnika funkcji w vtable klasy, i dlatego nie otrzymujesz błąd kompilatora. Przestrzeń w tabeli vtable jest nadal przydzielana i jest to miejsce, do którego faktycznie wracasz.

Ma to sens, ponieważ klasy pochodne zasadniczo zastępują te wartości za pomocą wskaźników do ich funkcji. Dlatego w drugim przykładzie działa (a->*f)() - f odwołuje się do tabeli vtable zaimplementowanej w klasie pochodnej.

+1

Mogło tak być, gdyby istniała separacja między wskaźnikami do zwykłych funkcji składowych a wskaźnikami do funkcji wirtualnego członka. Jednak, jak już wspomniałem, nie ma i właśnie o to chodzi. –

+0

Kompilator jest zdecydowanie dozwolone, aby umieścić wszystkie metody, wirtualne lub nie w vtable. Jeśli to zrobi, może następnie użyć indeksu vtable dla wskaźników do funkcji składowych. Jest to dość proste dla kompilatora - wystarczy upewnić się, że nadmiarowiec nie-wirtualny otrzymuje własny wpis vtable zamiast zastępowania wpisu klasy podstawowej. – MSalters