W jaki sposób wdrażane są funkcje wirtualne i vtable?

110

Wszyscy wiemy, jakie funkcje wirtualne są w C ++, ale jak są wdrażane na głębokim poziomie?

Czy tabela vtable może być modyfikowana lub nawet dostępna bezpośrednio w czasie wykonywania?

Czy tabela vtable istnieje dla wszystkich klas, czy tylko tych, które mają co najmniej jedną funkcję wirtualną?

Czy klasy abstrakcyjne mają po prostu wartość NULL dla wskaźnika funkcji co najmniej jednego wpisu?

Czy posiadanie jednej funkcji wirtualnej spowalnia całą klasę? Czy tylko wywołanie funkcji wirtualnej? I czy ma to wpływ na prędkość, jeśli funkcja wirtualna zostanie faktycznie nadpisana, czy nie, czy też nie ma to żadnego wpływu, o ile jest wirtualna.

Brian R. Bondy
źródło
2
Zaproponuj przeczytanie arcydzieła Inside the C++ Object Modelwg Stanley B. Lippman. (Rozdział 4.2, strony 124-131)
smwikipedia

Odpowiedzi:

123

W jaki sposób funkcje wirtualne są wdrażane na głębokim poziomie?

Z „funkcji wirtualnych w C ++” :

Zawsze, gdy program ma zadeklarowaną funkcję wirtualną, tworzona jest tablica av dla klasy. Tabela v-table składa się z adresów do funkcji wirtualnych dla klas, które zawierają jedną lub więcej funkcji wirtualnych. Obiekt klasy zawierającej funkcję wirtualną zawiera wirtualny wskaźnik, który wskazuje na adres bazowy wirtualnej tablicy w pamięci. Ilekroć występuje wywołanie funkcji wirtualnej, tabela v-table jest używana do rozstrzygania na adres funkcji. Obiekt klasy, który zawiera jedną lub więcej funkcji wirtualnych, zawiera wirtualny wskaźnik zwany vptr na samym początku obiektu w pamięci. Stąd rozmiar obiektu w tym przypadku rośnie wraz z rozmiarem wskaźnika. Ten vptr zawiera adres podstawowy tabeli wirtualnej w pamięci. Zwróć uwagę, że wirtualne tabele są specyficzne dla klasy, tj. istnieje tylko jedna wirtualna tabela dla klasy, niezależnie od liczby zawartych w niej funkcji wirtualnych. Ta wirtualna tabela z kolei zawiera adresy bazowe jednej lub więcej funkcji wirtualnych tej klasy. W momencie wywołania funkcji wirtualnej na obiekcie, vptr tego obiektu dostarcza adres bazowy tabeli wirtualnej dla tej klasy w pamięci. Ta tabela jest używana do rozwiązywania wywołań funkcji, ponieważ zawiera adresy wszystkich funkcji wirtualnych tej klasy. Oto jak dynamiczne wiązanie jest rozwiązywane podczas wywołania funkcji wirtualnej. vptr tego obiektu dostarcza adres bazowy tabeli wirtualnej dla tej klasy w pamięci. Ta tabela jest używana do rozwiązywania wywołań funkcji, ponieważ zawiera adresy wszystkich funkcji wirtualnych tej klasy. Oto jak dynamiczne wiązanie jest rozwiązywane podczas wywołania funkcji wirtualnej. vptr tego obiektu dostarcza adres bazowy tabeli wirtualnej dla tej klasy w pamięci. Ta tabela jest używana do rozwiązywania wywołań funkcji, ponieważ zawiera adresy wszystkich funkcji wirtualnych tej klasy. Oto jak dynamiczne wiązanie jest rozwiązywane podczas wywołania funkcji wirtualnej.

Czy tabela vtable może być modyfikowana lub nawet dostępna bezpośrednio w czasie wykonywania?

Powszechnie uważam, że odpowiedź brzmi „nie”. Możesz zrobić trochę zniekształcenia pamięci, aby znaleźć vtable, ale nadal nie wiesz, jak wygląda sygnatura funkcji, aby ją wywołać. Wszystko, co chciałbyś osiągnąć dzięki tej możliwości (obsługiwanej przez język), powinno być możliwe bez bezpośredniego dostępu do tabeli vtable lub modyfikowania jej w czasie wykonywania. Należy również zauważyć, że specyfikacja języka C ++ nie określa, że ​​vtables są wymagane - jednak w ten sposób większość kompilatorów implementuje funkcje wirtualne.

Czy tabela vtable istnieje dla wszystkich obiektów, czy tylko tych, które mają co najmniej jedną funkcję wirtualną?

Uważam , że odpowiedź brzmi "to zależy od implementacji", ponieważ specyfikacja nie wymaga przede wszystkim vtables. Jednak w praktyce uważam, że wszystkie współczesne kompilatory tworzą tabelę vtable tylko wtedy, gdy klasa ma co najmniej 1 funkcję wirtualną. Istnieje narzut przestrzeni związany z tabelą vtable i narzut czasowy związany z wywołaniem funkcji wirtualnej w porównaniu z funkcją niewirtualną.

Czy klasy abstrakcyjne mają po prostu wartość NULL dla wskaźnika funkcji co najmniej jednego wpisu?

Odpowiedź brzmi: nie jest określony w specyfikacji języka, więc zależy to od implementacji. Wywołanie czystej funkcji wirtualnej powoduje niezdefiniowane zachowanie, jeśli nie jest ona zdefiniowana (co zwykle nie jest) (ISO / IEC 14882: 2003 10.4-2). W praktyce alokuje miejsce w tabeli vtable dla funkcji, ale nie przypisuje jej adresu. To pozostawia niekompletną tabelę vtable, która wymaga od klas pochodnych zaimplementowania funkcji i uzupełnienia tabeli vtable. Niektóre implementacje po prostu umieszczają wskaźnik NULL we wpisie vtable; inne implementacje umieszczają wskaźnik na fikcyjną metodę, która robi coś podobnego do asercji.

Należy zauważyć, że klasa abstrakcyjna może definiować implementację czystej funkcji wirtualnej, ale ta funkcja może być wywoływana tylko ze składnią kwalifikowanego identyfikatora (tj. Z pełnym określeniem klasy w nazwie metody, podobnie jak wywołanie metody klasy bazowej z Klasy pochodnej). Ma to na celu zapewnienie łatwej w użyciu domyślnej implementacji, przy jednoczesnym wymaganiu, aby klasa pochodna zapewniała przesłonięcie.

Czy posiadanie pojedynczej funkcji wirtualnej spowalnia całą klasę, czy tylko wywołanie funkcji, która jest wirtualna?

To jest na granicy mojej wiedzy, więc jeśli się mylę, niech mi ktoś pomoże!

Uważam , że tylko funkcje wirtualne w klasie doświadczają uderzenia wydajności związanego z wywołaniem funkcji wirtualnej w porównaniu z funkcją niewirtualną. Tak czy inaczej, przestrzeń na zajęcia jest dostępna. Zauważ, że jeśli istnieje tabela vtable, jest tylko 1 na klasę , a nie jeden na obiekt .

Czy ma to wpływ na prędkość, jeśli funkcja wirtualna zostanie faktycznie zastąpiona lub nie, czy też nie ma to żadnego wpływu, o ile jest wirtualna?

Nie wierzę, że czas wykonywania funkcji wirtualnej, która jest zastępowana, skraca się w porównaniu z wywołaniem podstawowej funkcji wirtualnej. Istnieje jednak dodatkowe obciążenie miejsca dla klasy związane z definiowaniem innej tabeli vtable dla klasy pochodnej w porównaniu z klasą bazową.

Dodatkowe zasoby:

http://www.codersource.net/published/view/325/virtual_functions_in.aspx (za pośrednictwem maszyny powrotnej)
http://en.wikipedia.org/wiki/Virtual_table
http://www.codesourcery.com/public/ cxx-abi / abi.html # vtable

Zach Burlingame
źródło
2
Nie byłoby zgodne z filozofią C ++ Stroustrupa, gdyby kompilator umieszczał niepotrzebny wskaźnik vtable w obiekcie, który go nie potrzebuje. Zasada jest taka, że ​​nie otrzymujesz narzutów, których nie ma w C, chyba że o to poprosisz, a kompilatory są niegrzeczne, aby to przełamać.
Steve Jessop,
3
Zgadzam się, że byłoby głupotą dla każdego kompilatora, który traktuje siebie poważnie, używanie tabeli vtable, gdy nie istnieją żadne funkcje wirtualne. Jednak czułem, że ważne jest, aby zwrócić uwagę, że według mojej wiedzy standard C ++ nie wymaga / nie wymaga / tego, więc ostrzegaj przed uzależnieniem od niego.
Zach Burlingame,
8
Nawet funkcje wirtualne można nazwać nie-wirtualnymi. W rzeczywistości jest to dość powszechne: jeśli obiekt znajduje się na stosie, kompilator w zakresie będzie znał dokładny typ i zoptymalizuje wyszukiwanie vtable. Jest to szczególnie prawdziwe w przypadku dtor, który musi być wywoływany w tym samym zakresie stosu.
MSalters,
1
Uważam, że gdy klasa, która ma co najmniej jedną funkcję wirtualną, każdy obiekt ma tabelę vtable, a nie jedną dla całej klasy.
Asaf R
3
Typowa implementacja: każdy obiekt ma wskaźnik do tabeli vtable; klasa jest właścicielem tabeli. Magia konstrukcji polega po prostu na aktualizacji wskaźnika vtable w ctor pochodnym, po zakończeniu działania ctora podstawowego.
MSalters
31
  • Czy tabela vtable może być modyfikowana lub nawet dostępna bezpośrednio w czasie wykonywania?

Nie przenośnie, ale jeśli nie masz nic przeciwko brudnym sztuczkom, jasne!

OSTRZEŻENIE : Ta technika nie jest zalecana do stosowania przez dzieci, osoby dorosłe w wieku poniżej 969 lat lub małe futrzane stworzenia z Alpha Centauri. Efekty uboczne mogą obejmować demony, które wylatują z twojego nosa , nagłe pojawienie się Yog-Sothotha jako wymaganej osoby zatwierdzającej wszystkie kolejne przeglądy kodu lub wsteczne dodanie IHuman::PlayPiano()wszystkich istniejących instancji]

W większości kompilatorów, które widziałem, vtbl * to pierwsze 4 bajty obiektu, a zawartość vtbl to po prostu tablica wskaźników składowych (zazwyczaj w kolejności, w której zostały zadeklarowane, z pierwszą klasą bazową). Istnieją oczywiście inne możliwe układy, ale to właśnie zwykle obserwowałem.

class A {
  public:
  virtual int f1() = 0;
};
class B : public A {
  public:
  virtual int f1() { return 1; }
  virtual int f2() { return 2; }
};
class C : public A {
  public:
  virtual int f1() { return -1; }
  virtual int f2() { return -2; }
};

A *x = new B;
A *y = new C;
A *z = new C;

A teraz, żeby wyciągnąć kilka shenanigans ...

Zmiana klasy w czasie wykonywania:

std::swap(*(void **)x, *(void **)y);
// Now x is a C, and y is a B! Hope they used the same layout of members!

Zastępowanie metody dla wszystkich instancji (małpa na klasę)

To jest trochę trudniejsze, ponieważ sam vtbl jest prawdopodobnie w pamięci tylko do odczytu.

int f3(A*) { return 0; }

mprotect(*(void **)x,8,PROT_READ|PROT_WRITE|PROT_EXEC);
// Or VirtualProtect on win32; this part's very OS-specific
(*(int (***)(A *)x)[0] = f3;
// Now C::f1() returns 0 (remember we made x into a C above)
// so x->f1() and z->f1() both return 0

To ostatnie może raczej spowodować, że programy antywirusowe i łącze obudzą się i zwrócą uwagę, z powodu manipulacji mprotect. W procesie używającym bitu NX może się to nie udać.

puetzk
źródło
6
Hmm. Wydaje się złowieszcze, że otrzymało to nagrodę. Mam nadzieję, że to nie znaczy, że @Mobilewits uważa, że ​​takie shenanigans to naprawdę dobry pomysł ...
puetzk
1
Prosimy o rozważenie raczej zniechęcenia do używania tej techniki, wyraźnego i zdecydowanego, niż „mrugnięcia”.
einpoklum
Zawartość vtbl to po prostu tablica wskaźników do elementów członkowskich ” w rzeczywistości jest to rekord (struktura) z różnymi wpisami, które są równomiernie rozmieszczone
ciekawy facet
1
Możesz na to spojrzeć w dowolny sposób; wskaźniki funkcji mają różne sygnatury, a tym samym różne typy wskaźników; w tym sensie jest rzeczywiście podobny do struktury. Ale w innych kontekstach, ale idea indeksu vtbl jest przydatna (np. ActiveX używa go w sposób, w jaki opisuje podwójne interfejsy w typelibs), który jest bardziej podobny do tablicy.
puetzk
17

Czy posiadanie jednej funkcji wirtualnej spowalnia całą klasę?

Czy tylko wywołanie funkcji wirtualnej? I czy ma to wpływ na prędkość, jeśli funkcja wirtualna zostanie faktycznie nadpisana, czy nie, czy też nie ma to żadnego wpływu, o ile jest wirtualna.

Posiadanie funkcji wirtualnych spowalnia całą klasę o tyle, o ile trzeba zainicjować, skopiować,… kiedy mamy do czynienia z obiektem takiej klasy, jeszcze jeden element danych. W przypadku klasy liczącej mniej więcej pół tuzina członków różnica powinna być pomijalna. W przypadku klasy, która zawiera tylko jednego charczłonka lub nie zawiera żadnych elementów członkowskich, różnica może być zauważalna.

Poza tym należy zauważyć, że nie każde wywołanie funkcji wirtualnej jest wywołaniem funkcji wirtualnej. Jeśli masz obiekt znanego typu, kompilator może wyemitować kod dla normalnego wywołania funkcji, a nawet może wbudować tę funkcję, jeśli ma na to ochotę. Tylko wtedy, gdy wykonujesz wywołania polimorficzne, za pomocą wskaźnika lub referencji, które mogą wskazywać na obiekt klasy bazowej lub obiekt jakiejś klasy pochodnej, potrzebujesz pośredniej vtable i płacisz za nią w kategoriach wydajności.

struct Foo { virtual ~Foo(); virtual int a() { return 1; } };
struct Bar: public Foo { int a() { return 2; } };
void f(Foo& arg) {
  Foo x; x.a(); // non-virtual: always calls Foo::a()
  Bar y; y.a(); // non-virtual: always calls Bar::a()
  arg.a();      // virtual: must dispatch via vtable
  Foo z = arg;  // copy constructor Foo::Foo(const Foo&) will convert to Foo
  z.a();        // non-virtual Foo::a, since z is a Foo, even if arg was not
}

Kroki, które musi wykonać sprzęt, są zasadniczo takie same, niezależnie od tego, czy funkcja zostanie nadpisana, czy nie. Adres tabeli vtable jest odczytywany z obiektu, wskaźnik funkcji jest pobierany z odpowiedniego gniazda, a funkcja wywoływana przez wskaźnik. Jeśli chodzi o rzeczywistą wydajność, pewien wpływ mogą mieć prognozy branżowe. Na przykład, jeśli większość twoich obiektów odwołuje się do tej samej implementacji danej funkcji wirtualnej, to istnieje pewna szansa, że ​​predyktor rozgałęzienia poprawnie przewidział, którą funkcję wywołać, nawet zanim wskaźnik zostanie pobrany. Ale nie ma znaczenia, która funkcja jest powszechna: może to być większość obiektów delegowanych do niepodpisanego przypadku podstawowego lub większość obiektów należących do tej samej podklasy, a zatem delegujących do tego samego nadpisanego przypadku.

jak są wdrażane na głębokim poziomie?

Podoba mi się pomysł jheriko, aby zademonstrować to za pomocą próbnej implementacji. Ale użyłbym C do zaimplementowania czegoś podobnego do powyższego kodu, aby niski poziom był łatwiejszy do zobaczenia.

klasa nadrzędna Foo

typedef struct Foo_t Foo;   // forward declaration
struct slotsFoo {           // list all virtual functions of Foo
  const void *parentVtable; // (single) inheritance
  void (*destructor)(Foo*); // virtual destructor Foo::~Foo
  int (*a)(Foo*);           // virtual function Foo::a
};
struct Foo_t {                      // class Foo
  const struct slotsFoo* vtable;    // each instance points to vtable
};
void destructFoo(Foo* self) { }     // Foo::~Foo
int aFoo(Foo* self) { return 1; }   // Foo::a()
const struct slotsFoo vtableFoo = { // only one constant table
  0,                                // no parent class
  destructFoo,
  aFoo
};
void constructFoo(Foo* self) {      // Foo::Foo()
  self->vtable = &vtableFoo;        // object points to class vtable
}
void copyConstructFoo(Foo* self,
                      Foo* other) { // Foo::Foo(const Foo&)
  self->vtable = &vtableFoo;        // don't copy from other!
}

klasa pochodna Bar

typedef struct Bar_t {              // class Bar
  Foo base;                         // inherit all members of Foo
} Bar;
void destructBar(Bar* self) { }     // Bar::~Bar
int aBar(Bar* self) { return 2; }   // Bar::a()
const struct slotsFoo vtableBar = { // one more constant table
  &vtableFoo,                       // can dynamic_cast to Foo
  (void(*)(Foo*)) destructBar,      // must cast type to avoid errors
  (int(*)(Foo*)) aBar
};
void constructBar(Bar* self) {      // Bar::Bar()
  self->base.vtable = &vtableBar;   // point to Bar vtable
}

funkcja f wykonująca wywołanie funkcji wirtualnej

void f(Foo* arg) {                  // same functionality as above
  Foo x; constructFoo(&x); aFoo(&x);
  Bar y; constructBar(&y); aBar(&y);
  arg->vtable->a(arg);              // virtual function call
  Foo z; copyConstructFoo(&z, arg);
  aFoo(&z);
  destructFoo(&z);
  destructBar(&y);
  destructFoo(&x);
}

Jak więc widać, vtable to tylko statyczny blok w pamięci, zawierający głównie wskaźniki do funkcji. Każdy obiekt klasy polimorficznej będzie wskazywał na tabelę vtable odpowiadającą jego typowi dynamicznemu. To również sprawia, że ​​połączenie między RTTI a funkcjami wirtualnymi jest wyraźniejsze: możesz sprawdzić, jakiego typu jest klasa, po prostu patrząc, na którą vtable wskazuje. Powyższe jest uproszczone na wiele sposobów, jak np. Dziedziczenie wielokrotne, ale ogólna koncepcja jest rozsądna.

Jeśli argjest typu Foo*i bierzesz arg->vtable, ale w rzeczywistości jest obiektem typu Bar, nadal uzyskujesz poprawny adres vtable. Dzieje się tak dlatego, że vtablejest zawsze pierwszym elementem pod adresem obiektu, bez względu na to, czy jest wywoływany, vtableczy base.vtablew poprawnie wpisanym wyrażeniu.

MvG
źródło
„Każdy obiekt klasy polimorficznej będzie wskazywał na swoją własną tabelę vtable”. Czy chcesz powiedzieć, że każdy obiekt ma swoją własną tabelę vtable? AFAIK vtable jest współużytkowany przez wszystkie obiekty tej samej klasy. Daj mi znać, jeśli się mylę.
Bhuwan,
1
@Bhuwan: Nie, masz rację: istnieje tylko jedna tabela vtable na typ (która może być instancją na szablon w przypadku szablonów). Chciałem powiedzieć, że każdy obiekt klasy polimorficznej wskazuje na tabelę vtable, która go dotyczy, więc każdy obiekt ma taki wskaźnik, ale dla obiektów tego samego typu będzie wskazywał na tę samą tabelę. Prawdopodobnie powinienem to przeredagować.
MvG,
1
@MvG " obiekty tego samego typu, które będą wskazywały na tę samą tabelę " nie podczas konstruowania klas bazowych z wirtualnymi klasami bazowymi! (bardzo szczególny przypadek)
ciekawy.
1
@curiousguy: zgłosiłbym to pod hasłem „powyższe jest uproszczone na wiele sposobów”, zwłaszcza że głównym zastosowaniem wirtualnych baz jest wielokrotne dziedziczenie, którego też nie modelowałem. Ale dzięki za komentarz, warto mieć go tutaj dla ludzi, którzy mogą potrzebować więcej głębi.
MvG,
3

Zwykle z tabelą VTable, tablicą wskaźników do funkcji.

Lou Franco
źródło
2

Ta odpowiedź została włączona do odpowiedzi na Wiki społeczności

  • Czy klasy abstrakcyjne mają po prostu wartość NULL dla wskaźnika funkcji co najmniej jednego wpisu?

Odpowiedź jest taka, że ​​jest nieokreślona - wywołanie czystej funkcji wirtualnej powoduje niezdefiniowane zachowanie, jeśli nie jest ona zdefiniowana (co zwykle nie jest) (ISO / IEC 14882: 2003 10.4-2). Niektóre implementacje po prostu umieszczają wskaźnik NULL we wpisie vtable; inne implementacje umieszczają wskaźnik na fikcyjną metodę, która robi coś podobnego do asercji.

Należy zauważyć, że klasa abstrakcyjna może definiować implementację czystej funkcji wirtualnej, ale ta funkcja może być wywoływana tylko ze składnią kwalifikowanego identyfikatora (tj. Z pełnym określeniem klasy w nazwie metody, podobnie jak wywołanie metody klasy bazowej z Klasy pochodnej). Ma to na celu zapewnienie łatwej w użyciu domyślnej implementacji, przy jednoczesnym wymaganiu, aby klasa pochodna zapewniała przesłonięcie.

Michael Burr
źródło
Nie sądzę również, aby klasa abstrakcyjna mogła definiować implementację czystej funkcji wirtualnej. Z definicji czysta funkcja wirtualna nie ma treści (np. Bool my_func () = 0;). Możesz jednak zapewnić implementacje dla zwykłych funkcji wirtualnych.
Zach Burlingame,
Czysta funkcja wirtualna może mieć definicję. Zobacz artykuł Scotta Meyersa „Effective C ++, 3rd Ed”, pozycja nr 34, ISO 14882-2003 10.4-2 lub bytes.com/forum/thread572745.html
Michael Burr,
2

Funkcjonalność funkcji wirtualnych można odtworzyć w języku C ++, używając wskaźników funkcji jako elementów członkowskich klasy i funkcji statycznych jako implementacji lub używając wskaźnika do funkcji składowych i funkcji składowych dla implementacji. Te dwie metody mają tylko zalety notacyjne ... w rzeczywistości wywołania funkcji wirtualnych same w sobie są tylko notacyjnym udogodnieniem. W rzeczywistości dziedziczenie to tylko notacyjna wygoda ... wszystko można zaimplementować bez używania funkcji języka do dziedziczenia. :)

Poniższy kod jest nietestowany i prawdopodobnie zawiera błędy, ale mam nadzieję, że demonstruje ten pomysł.

na przykład

class Foo
{
protected:
 void(*)(Foo*) MyFunc;
public:
 Foo() { MyFunc = 0; }
 void ReplciatedVirtualFunctionCall()
 {
  MyFunc(*this);
 }
...
};

class Bar : public Foo
{
private:
 static void impl1(Foo* f)
 {
  ...
 }
public:
 Bar() { MyFunc = impl1; }
...
};

class Baz : public Foo
{
private:
 static void impl2(Foo* f)
 {
  ...
 }
public:
 Baz() { MyFunc = impl2; }
...
};
jheriko
źródło
void(*)(Foo*) MyFunc;czy to jest jakaś składnia Javy?
curiousguy
nie, jego składnia C / C ++ dla wskaźników funkcji. Cytując siebie: „Możesz odtworzyć funkcjonalność funkcji wirtualnych w C ++ używając wskaźników do funkcji”. to paskudna składnia, ale coś, z czym należy się zapoznać, jeśli uważasz się za programistę C.
jheriko
wskaźnik funkcji ac wyglądałby bardziej jak: int ( PROC) (); a wskaźnik do funkcji składowej klasy wyglądałby następująco: int (ClassName :: MPROC) ();
Menace,
1
@menace, zapomniałeś tam jakiejś składni ... myślisz może o typedef? typedef int (* PROC) (); więc możesz po prostu wykonać PROC foo później zamiast int (* foo) ()?
jheriko
2

Postaram się to uprościć :)

Wszyscy wiemy, jakie funkcje wirtualne są w C ++, ale jak są wdrażane na głębokim poziomie?

Jest to tablica ze wskaźnikami do funkcji, które są implementacjami określonej funkcji wirtualnej. Indeks w tej tablicy reprezentuje określony indeks funkcji wirtualnej zdefiniowanej dla klasy. Obejmuje to czyste funkcje wirtualne.

Gdy klasa polimorficzna wywodzi się z innej klasy polimorficznej, możemy mieć następujące sytuacje:

  • Klasa pochodna nie dodaje nowych funkcji wirtualnych ani nie przesłania żadnych. W tym przypadku ta klasa dzieli vtable z klasą bazową.
  • Klasa pochodna dodaje i przesłania metody wirtualne. W tym przypadku otrzymuje własną tabelę vtable, w której dodane funkcje wirtualne mają indeks zaczynający się od ostatniego pochodnego.
  • Wiele klas polimorficznych w dziedziczeniu. W tym przypadku mamy przesunięcie indeksu między drugą a następną bazą i jej indeks w klasie pochodnej

Czy tabela vtable może być modyfikowana lub nawet dostępna bezpośrednio w czasie wykonywania?

Niestandardowy sposób - nie ma API, aby uzyskać do nich dostęp. Kompilatory mogą mieć pewne rozszerzenia lub prywatne interfejsy API, aby uzyskać do nich dostęp, ale może to być tylko rozszerzenie.

Czy tabela vtable istnieje dla wszystkich klas, czy tylko tych, które mają co najmniej jedną funkcję wirtualną?

Tylko te, które mają co najmniej jedną funkcję wirtualną (czy to nawet destruktor) lub wyprowadzają co najmniej jedną klasę, która ma swoją vtable („jest polimorficzna”).

Czy klasy abstrakcyjne mają po prostu wartość NULL dla wskaźnika funkcji co najmniej jednego wpisu?

To możliwa implementacja, ale raczej nie jest praktykowana. Zamiast tego zwykle istnieje funkcja, która wyświetla coś w rodzaju „czystej funkcji wirtualnej o nazwie” i robi abort(). Wywołanie tego może wystąpić, jeśli spróbujesz wywołać metodę abstrakcyjną w konstruktorze lub destruktorze.

Czy posiadanie jednej funkcji wirtualnej spowalnia całą klasę? Czy tylko wywołanie funkcji wirtualnej? I czy ma to wpływ na prędkość, jeśli funkcja wirtualna zostanie faktycznie nadpisana, czy nie, czy też nie ma to żadnego wpływu, o ile jest wirtualna.

Spowolnienie jest zależne tylko od tego, czy połączenie zostanie rozwiązane jako połączenie bezpośrednie, czy jako połączenie wirtualne. I nic innego się nie liczy. :)

Jeśli wywołasz funkcję wirtualną za pomocą wskaźnika lub odwołania do obiektu, to zawsze zostanie ona zaimplementowana jako wywołanie wirtualne - ponieważ kompilator nigdy nie będzie wiedział, jaki rodzaj obiektu zostanie przypisany do tego wskaźnika w czasie wykonywania i czy jest to klasa, w której ta metoda jest przesłonięta lub nie. Tylko w dwóch przypadkach kompilator może rozpoznać wywołanie funkcji wirtualnej jako wywołanie bezpośrednie:

  • Jeśli wywołasz metodę przez wartość (zmienną lub wynik funkcji, która zwraca wartość) - w tym przypadku kompilator nie ma wątpliwości, jaka jest rzeczywista klasa obiektu i może ją „na twardo rozwiązać” w czasie kompilacji .
  • Jeśli metoda wirtualna jest zadeklarowana finalw klasie, do której masz wskaźnik lub odwołanie, przez które ją wywołujesz ( tylko w C ++ 11 ). W tym przypadku kompilator wie, że ta metoda nie może podlegać dalszemu nadpisywaniu i może to być tylko metoda z tej klasy.

Należy jednak pamiętać, że wywołania wirtualne mają tylko narzut związany z wyłuskiwaniem dwóch wskaźników. Używanie RTTI (chociaż dostępne tylko dla klas polimorficznych) jest wolniejsze niż wywoływanie metod wirtualnych, jeśli znajdziesz przypadek, aby zaimplementować to samo na dwa sposoby. Na przykład zdefiniowanie, virtual bool HasHoof() { return false; }a następnie zastąpienie tylko w bool Horse::HasHoof() { return true; }taki sposób, aby zapewnić Ci możliwość wywołania if (anim->HasHoof()), będzie szybsze niż próbowanie if(dynamic_cast<Horse*>(anim)). Dzieje się tak, ponieważ dynamic_castw niektórych przypadkach trzeba przejść przez hierarchię klas, nawet rekurencyjnie, aby zobaczyć, czy można zbudować ścieżkę na podstawie rzeczywistego typu wskaźnika i żądanego typu klasy. Podczas gdy wirtualne połączenie jest zawsze takie samo - wyłuskiwanie dwóch wskaźników.

Ethouris
źródło
2

Oto uruchamialna ręczna implementacja wirtualnej tabeli w nowoczesnym C ++. Ma dobrze zdefiniowaną semantykę, bez hacków i nievoid* .

Uwaga: .*i ->*są innymi operatorami niż *i ->. Wskaźniki funkcji składowej działają inaczej.

#include <iostream>
#include <vector>
#include <memory>

struct vtable; // forward declare, we need just name

class animal
{
public:
    const std::string& get_name() const { return name; }

    // these will be abstract
    bool has_tail() const;
    bool has_wings() const;
    void sound() const;

protected: // we do not want animals to be created directly
    animal(const vtable* vtable_ptr, std::string name)
    : vtable_ptr(vtable_ptr), name(std::move(name)) { }

private:
    friend vtable; // just in case for non-public methods

    const vtable* const vtable_ptr;
    std::string name;
};

class cat : public animal
{
public:
    cat(std::string name);

    // functions to bind dynamically
    bool has_tail() const { return true; }
    bool has_wings() const { return false; }
    void sound() const
    {
        std::cout << get_name() << " does meow\n"; 
    }
};

class dog : public animal
{
public:
    dog(std::string name);

    // functions to bind dynamically
    bool has_tail() const { return true; }
    bool has_wings() const { return false; }
    void sound() const
    {
        std::cout << get_name() << " does whoof\n"; 
    }
};

class parrot : public animal
{
public:
    parrot(std::string name);

    // functions to bind dynamically
    bool has_tail() const { return false; }
    bool has_wings() const { return true; }
    void sound() const
    {
        std::cout << get_name() << " does crrra\n"; 
    }
};

// now the magic - pointers to member functions!
struct vtable
{
    bool (animal::* const has_tail)() const;
    bool (animal::* const has_wings)() const;
    void (animal::* const sound)() const;

    // constructor
    vtable (
        bool (animal::* const has_tail)() const,
        bool (animal::* const has_wings)() const,
        void (animal::* const sound)() const
    ) : has_tail(has_tail), has_wings(has_wings), sound(sound) { }
};

// global vtable objects
const vtable vtable_cat(
    static_cast<bool (animal::*)() const>(&cat::has_tail),
    static_cast<bool (animal::*)() const>(&cat::has_wings),
    static_cast<void (animal::*)() const>(&cat::sound));
const vtable vtable_dog(
    static_cast<bool (animal::*)() const>(&dog::has_tail),
    static_cast<bool (animal::*)() const>(&dog::has_wings),
    static_cast<void (animal::*)() const>(&dog::sound));
const vtable vtable_parrot(
    static_cast<bool (animal::*)() const>(&parrot::has_tail),
    static_cast<bool (animal::*)() const>(&parrot::has_wings),
    static_cast<void (animal::*)() const>(&parrot::sound));

// set vtable pointers in constructors
cat::cat(std::string name) : animal(&vtable_cat, std::move(name)) { }
dog::dog(std::string name) : animal(&vtable_dog, std::move(name)) { }
parrot::parrot(std::string name) : animal(&vtable_parrot, std::move(name)) { }

// implement dynamic dispatch
bool animal::has_tail() const
{
    return (this->*(vtable_ptr->has_tail))();
}

bool animal::has_wings() const
{
    return (this->*(vtable_ptr->has_wings))();
}

void animal::sound() const
{
    (this->*(vtable_ptr->sound))();
}

int main()
{
    std::vector<std::unique_ptr<animal>> animals;
    animals.push_back(std::make_unique<cat>("grumpy"));
    animals.push_back(std::make_unique<cat>("nyan"));
    animals.push_back(std::make_unique<dog>("doge"));
    animals.push_back(std::make_unique<parrot>("party"));

    for (const auto& a : animals)
        a->sound();

    // note: destructors are not dispatched virtually
}
Xeverous
źródło
1

Każdy obiekt ma wskaźnik vtable, który wskazuje na tablicę funkcji składowych.


źródło
1

We wszystkich tych odpowiedziach nie wspomniano tutaj o tym, że w przypadku dziedziczenia wielokrotnego, gdzie wszystkie klasy bazowe mają metody wirtualne. Klasa dziedzicząca ma wiele wskaźników do maszyny wirtualnej. W rezultacie rozmiar każdego wystąpienia takiego obiektu jest większy. Każdy wie, że klasa z metodami wirtualnymi ma dodatkowe 4 bajty na vmt, ale w przypadku wielokrotnego dziedziczenia jest to dla każdej klasy bazowej, która ma metody wirtualne razy 4. 4 to rozmiar wskaźnika.

Philip Stuyck
źródło
0

Odpowiedzi Burly są poprawne, z wyjątkiem pytania:

Czy klasy abstrakcyjne mają po prostu wartość NULL dla wskaźnika funkcji co najmniej jednego wpisu?

Odpowiedź jest taka, że ​​dla klas abstrakcyjnych w ogóle nie jest tworzona wirtualna tabela. Nie ma takiej potrzeby, ponieważ nie można tworzyć obiektów tych klas!

Innymi słowy, jeśli mamy:

class B { ~B() = 0; }; // Abstract Base class
class D : public B { ~D() {} }; // Concrete Derived class

D* pD = new D();
B* pB = pD;

Wskaźnik vtbl, do którego można uzyskać dostęp przez pB, będzie wskaźnikiem vtbl klasy D. Dokładnie w ten sposób implementowany jest polimorfizm. To znaczy, w jaki sposób metody D są dostępne przez pB. Nie ma potrzeby stosowania vtbl dla klasy B.

W odpowiedzi na komentarz Mike'a poniżej ...

Jeśli klasa B w moim opisie ma wirtualną metodę foo (), która nie jest nadpisywana przez D i wirtualną metodę bar (), która jest nadpisywana, to vtbl D będzie miał wskaźnik do foo () B i własnego paska () . Nadal nie ma utworzonego vtbl dla B.

Andrew Stein
źródło
Nie jest to poprawne z 2 powodów: 1) klasa abstrakcyjna może mieć zwykłe metody wirtualne oprócz czystych metod wirtualnych oraz 2) czyste metody wirtualne mogą opcjonalnie mieć definicję, którą można wywołać z w pełni kwalifikowaną nazwą.
Michael Burr,
Zaraz po zastanowieniu wyobrażam sobie, że gdyby wszystkie metody wirtualne były czysto wirtualne, kompilator mógłby zoptymalizować tabelę vtable (potrzebowałby pomocy ze strony linkera, aby upewnić się, że nie ma również definicji).
Michael Burr,
1
Odpowiedź jest taka, że ​​w ogóle nie tworzy się wirtualnej tabeli dla klas abstrakcyjnych. ” Błąd. „ Nie ma takiej potrzeby, ponieważ nie można tworzyć obiektów tych klas! ” Źle.
curiousguy
Mogę podążać za twoim uzasadnieniem, że żadna tabela nie B powinna być potrzebna. To, że niektóre z jego metod mają (domyślne) implementacje, nie oznacza, że ​​muszą być przechowywane w tabeli vtable. Ale właśnie uruchomiłem twój kod (modulo kilka poprawek, aby go skompilować), gcc -Sa następnie c++filti wyraźnie jest tam Bdołączona tabela vtable . Myślę, że może to być spowodowane tym, że vtable przechowuje również dane RTTI, takie jak nazwy klas i dziedziczenie. Może to być wymagane w przypadku pliku dynamic_cast<B*>. Nawet -fno-rttinie sprawia, że ​​vtable zniknie. Dzięki clang -O3zamiast gccto nagle zniknął.
MvG,
@MvG „ Tylko dlatego, że niektóre z jego metod mają (domyślne) implementacje, nie oznacza to, że muszą być przechowywane w tabeli vtable ” Tak, to tylko tyle.
curiousguy
0

bardzo ładny dowód koncepcji, który zrobiłem nieco wcześniej (aby sprawdzić, czy kolejność dziedziczenia ma znaczenie); daj mi znać, jeśli Twoja implementacja C ++ faktycznie go odrzuca (moja wersja gcc daje tylko ostrzeżenie o przypisywaniu anonimowych struktur, ale to błąd), jestem ciekawy.

CCPolite.h :

#ifndef CCPOLITE_H
#define CCPOLITE_H

/* the vtable or interface */
typedef struct {
    void (*Greet)(void *);
    void (*Thank)(void *);
} ICCPolite;

/**
 * the actual "object" literal as C++ sees it; public variables be here too 
 * all CPolite objects use(are instances of) this struct's structure.
 */
typedef struct {
    ICCPolite *vtbl;
} CPolite;

#endif /* CCPOLITE_H */

CCPolite_constructor.h :

/** 
 * unconventionally include me after defining OBJECT_NAME to automate
 * static(allocation-less) construction.
 *
 * note: I assume CPOLITE_H is included; since if I use anonymous structs
 *     for each object, they become incompatible and cause compile time errors
 *     when trying to do stuff like assign, or pass functions.
 *     this is similar to how you can't pass void * to windows functions that
 *         take handles; these handles use anonymous structs to make 
 *         HWND/HANDLE/HINSTANCE/void*/etc not automatically convertible, and
 *         require a cast.
 */
#ifndef OBJECT_NAME
    #error CCPolite> constructor requires object name.
#endif

CPolite OBJECT_NAME = {
    &CCPolite_Vtbl
};

/* ensure no global scope pollution */
#undef OBJECT_NAME

main.c :

#include <stdio.h>
#include "CCPolite.h"

// | A Greeter is capable of greeting; nothing else.
struct IGreeter
{
    virtual void Greet() = 0;
};

// | A Thanker is capable of thanking; nothing else.
struct IThanker
{
    virtual void Thank() = 0;
};

// | A Polite is something that implements both IGreeter and IThanker
// | Note that order of implementation DOES MATTER.
struct IPolite1 : public IGreeter, public IThanker{};
struct IPolite2 : public IThanker, public IGreeter{};

// | implementation if IPolite1; implements IGreeter BEFORE IThanker
struct CPolite1 : public IPolite1
{
    void Greet()
    {
        puts("hello!");
    }

    void Thank()
    {
        puts("thank you!");
    }
};

// | implementation if IPolite1; implements IThanker BEFORE IGreeter
struct CPolite2 : public IPolite2
{
    void Greet()
    {
        puts("hi!");
    }

    void Thank()
    {
        puts("ty!");
    }
};

// | imposter Polite's Greet implementation.
static void CCPolite_Greet(void *)
{
    puts("HI I AM C!!!!");
}

// | imposter Polite's Thank implementation.
static void CCPolite_Thank(void *)
{
    puts("THANK YOU, I AM C!!");
}

// | vtable of the imposter Polite.
ICCPolite CCPolite_Vtbl = {
    CCPolite_Thank,
    CCPolite_Greet    
};

CPolite CCPoliteObj = {
    &CCPolite_Vtbl
};

int main(int argc, char **argv)
{
    puts("\npart 1");
    CPolite1 o1;
    o1.Greet();
    o1.Thank();

    puts("\npart 2");    
    CPolite2 o2;    
    o2.Greet();
    o2.Thank();    

    puts("\npart 3");    
    CPolite1 *not1 = (CPolite1 *)&o2;
    CPolite2 *not2 = (CPolite2 *)&o1;
    not1->Greet();
    not1->Thank();
    not2->Greet();
    not2->Thank();

    puts("\npart 4");        
    CPolite1 *fake = (CPolite1 *)&CCPoliteObj;
    fake->Thank();
    fake->Greet();

    puts("\npart 5");        
    CPolite2 *fake2 = (CPolite2 *)fake;
    fake2->Thank();
    fake2->Greet();

    puts("\npart 6");        
    #define OBJECT_NAME fake3
    #include "CCPolite_constructor.h"
    fake = (CPolite1 *)&fake3;
    fake->Thank();
    fake->Greet();

    puts("\npart 7");        
    #define OBJECT_NAME fake4
    #include "CCPolite_constructor.h"
    fake2 = (CPolite2 *)&fake4;
    fake2->Thank();
    fake2->Greet();    

    return 0;
}

wynik:

part 1
hello!
thank you!

part 2
hi!
ty!

part 3
ty!
hi!
thank you!
hello!

part 4
HI I AM C!!!!
THANK YOU, I AM C!!

part 5
THANK YOU, I AM C!!
HI I AM C!!!!

part 6
HI I AM C!!!!
THANK YOU, I AM C!!

part 7
THANK YOU, I AM C!!
HI I AM C!!!!

uwaga, ponieważ nigdy nie przydzielam fałszywego obiektu, nie ma potrzeby niszczenia; destruktory są automatycznie umieszczane na końcu zakresu dynamicznie przydzielanych obiektów, aby odzyskać pamięć samego literału obiektu i wskaźnika vtable.

Dmitrij
źródło