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.
c++
polymorphism
virtual-functions
vtable
Brian R. Bondy
źródło
źródło
Inside the C++ Object Model
wgStanley B. Lippman
. (Rozdział 4.2, strony 124-131)Odpowiedzi:
W jaki sposób funkcje wirtualne są wdrażane na głębokim poziomie?
Z „funkcji wirtualnych w C ++” :
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
źródło
Nie przenośnie, ale jeśli nie masz nic przeciwko brudnym sztuczkom, jasne!
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.
A teraz, żeby wyciągnąć kilka shenanigans ...
Zmiana klasy w czasie wykonywania:
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.
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ć.
źródło
Czy posiadanie jednej funkcji wirtualnej spowalnia całą klasę?
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
char
czł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.
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
klasa pochodna Bar
funkcja f wykonująca wywołanie funkcji wirtualnej
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
arg
jest typuFoo*
i bierzeszarg->vtable
, ale w rzeczywistości jest obiektem typuBar
, nadal uzyskujesz poprawny adresvtable
. Dzieje się tak dlatego, żevtable
jest zawsze pierwszym elementem pod adresem obiektu, bez względu na to, czy jest wywoływany,vtable
czybase.vtable
w poprawnie wpisanym wyrażeniu.źródło
Zwykle z tabelą VTable, tablicą wskaźników do funkcji.
źródło
Ta odpowiedź została włączona do odpowiedzi na Wiki społeczności
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.
źródło
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
źródło
void(*)(Foo*) MyFunc;
czy to jest jakaś składnia Javy?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:
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:
final
w 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 wbool Horse::HasHoof() { return true; }
taki sposób, aby zapewnić Ci możliwość wywołaniaif (anim->HasHoof())
, będzie szybsze niż próbowanieif(dynamic_cast<Horse*>(anim))
. Dzieje się tak, ponieważdynamic_cast
w 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.źródło
Oto uruchamialna ręczna implementacja wirtualnej tabeli w nowoczesnym C ++. Ma dobrze zdefiniowaną semantykę, bez hacków i nie
void*
.Uwaga:
.*
i->*
są innymi operatorami niż*
i->
. Wskaźniki funkcji składowej działają inaczej.źródło
Każdy obiekt ma wskaźnik vtable, który wskazuje na tablicę funkcji składowych.
źródło
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.
źródło
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:
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.
źródło
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 -S
a następniec++filt
i wyraźnie jest tamB
dołą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 plikudynamic_cast<B*>
. Nawet-fno-rtti
nie sprawia, że vtable zniknie. Dziękiclang -O3
zamiastgcc
to nagle zniknął.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 :
CCPolite_constructor.h :
main.c :
wynik:
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.
źródło