Tabulka virtuálních metod - Virtual method table
A tabulka virtuálních metod (VMT), tabulka virtuálních funkcí, tabulka virtuálních hovorů, odesílací tabulka, vtablenebo vftable je mechanismus používaný v a programovací jazyk podporovat dynamické odesílání (nebo run-time metoda vazba ).
Kdykoli třída definuje a virtuální funkce (nebo metoda), většina překladačů přidá do třídy skrytou členskou proměnnou, která ukazuje na pole ukazatelů na (virtuální) funkce zvané tabulka virtuální metody. Tyto ukazatele se používají za běhu k vyvolání příslušných implementací funkcí, protože v době kompilace ještě nemusí být známo, zda má být volána základní funkce nebo odvozená funkce implementovaná třídou, která dědí ze základní třídy.
Existuje mnoho různých způsobů implementace takového dynamického odesílání, ale použití tabulek virtuálních metod je mezi nimi obzvláště běžné C ++ a související jazyky (např D a C# ). Jazyky, které oddělují programové rozhraní objektů od implementace, například Visual Basic a Delphi, také mají tendenci používat tento přístup, protože umožňuje objektům použít jinou implementaci jednoduše pomocí jiné sady ukazatelů metod.
Předpokládejme, že program obsahuje tři třídy v dědictví hierarchie: a nadtřída, Kočka
a dva podtřídy, Domácí kočka
a Lev
. Třída Kočka
definuje a virtuální funkce pojmenovaný mluvit
, takže jeho podtřídy mohou poskytovat vhodnou implementaci (např mňoukat
nebo řev
). Když program volá mluvit
funkce na a Kočka
reference (což může odkazovat na instanci Kočka
, nebo instance Domácí kočka
nebo Lev
), kód musí být schopen určit, kterou implementaci funkce by mělo být volání odesláno na. To závisí na skutečné třídě objektu, nikoli na třídě odkazu na něj (Kočka
). Třídu nelze obecně určit staticky (tj. v čas kompilace ), takže ani kompilátor nemůže rozhodnout, kterou funkci má v té době volat. Hovor musí být odeslán na správnou funkci dynamicky (tj. v doba běhu ) namísto.
Implementace
Tabulka virtuálních metod objektu bude obsahovat adresy dynamicky vázaných metod objektu. Volání metody se provádí načtením adresy metody z tabulky virtuálních metod objektu. Tabulka virtuálních metod je stejná pro všechny objekty patřící do stejné třídy, a proto se mezi nimi obvykle sdílí. Objekty patřící do tříd kompatibilních s typem (například sourozenci v hierarchii dědičnosti) budou mít tabulky virtuálních metod se stejným rozložením: adresa dané metody se objeví ve stejném posunu pro všechny třídy kompatibilní s typem. Načtení adresy metody z daného posunu do tabulky virtuální metody tedy získá metodu odpovídající skutečné třídě objektu.[1]
The C ++ standardy neříkají přesně, jak musí být implementováno dynamické odesílání, ale překladače obecně používají menší varianty na stejném základním modelu.
Kompilátor obvykle vytvoří pro každou třídu samostatnou tabulku virtuálních metod. Při vytváření objektu se ukazatel na tuto tabulku nazývá ukazatel virtuální tabulky, vpointer nebo VPTR, je přidán jako skrytý člen tohoto objektu. Překladač jako takový musí také generovat „skrytý“ kód v souboru konstruktéři každé třídy inicializovat ukazatel nové tabulky virtuální tabulky na adresu tabulky virtuální metody své třídy.
Mnoho překladačů umístí ukazatel virtuální tabulky jako posledního člena objektu; ostatní překladači jej umístí jako první; přenosný zdrojový kód funguje v obou směrech.[2]Například, g ++ dříve umístil ukazatel na konec objektu.[3]
Příklad
Zvažte následující deklarace třídy v Syntaxe C ++:
třída B1 {veřejnost: virtuální ~B1() {} prázdnota f0() {} virtuální prázdnota f1() {} int int_in_b1;};třída B2 {veřejnost: virtuální ~B2() {} virtuální prázdnota f2() {} int int_in_b2;};
slouží k odvození následující třídy:
třída D : veřejnost B1, veřejnost B2 {veřejnost: prázdnota d() {} prázdnota f2() přepsat {} int int_in_d;};
a následující část kódu C ++:
B2 *b2 = Nový B2();D *d = Nový D();
g ++ 3.4.6 z GCC vytvoří následující 32bitové rozložení paměti pro objekt b2
:[poznámka 1]
b2: +0: ukazatel na tabulku virtuálních metod B2 +4: hodnota tabulky metod int_in_b2virtual B2: +0: B2 :: f2 ()
a následující rozložení paměti pro objekt d
:
d: +0: ukazatel na tabulku virtuálních metod D (pro B1) +4: hodnota int_in_b1 +8: ukazatel na tabulku virtuálních metod D (pro B2) +12: hodnota int_in_b2 +16: hodnota int_in_d Celková velikost: 20 Bytes.virtual metodická tabulka D (pro B1): +0: B1 :: f1 () // B1 :: f1 () není přepsána virtuální tabulka metod D (pro B2): +0: D :: f2 ( ) // B2 :: f2 () je přepsáno D :: f2 ()
Všimněte si, že tyto funkce nenesou klíčové slovo virtuální
ve svém prohlášení (např f0 ()
a d ()
) se obecně neobjevují v tabulce virtuálních metod. Existují výjimky pro zvláštní případy, které stanoví výchozí konstruktor.
Všimněte si také virtuální destruktory v základních třídách, B1
a B2
. Je nutné je zajistit smazat d
může uvolnit paměť nejen pro D
, ale také pro B1
a B2
, pokud d
je ukazatel nebo odkaz na typy B1
nebo B2
. Byli vyloučeni z rozložení paměti, aby byl příklad jednoduchý. [pozn. 2]
Přepsání metody f2 ()
ve třídě D
je implementován duplikováním tabulky virtuálních metod z B2
a nahrazení ukazatele na B2 :: f2 ()
s ukazatelem na D :: f2 ()
.
Vícenásobné dědictví a thunky
Kompilátor g ++ implementuje vícenásobné dědictví tříd B1
a B2
ve třídě D
pomocí dvou tabulek virtuálních metod, jedné pro každou základní třídu. (Existují i jiné způsoby, jak implementovat vícenásobné dědictví, ale toto je nejběžnější.) To vede k nutnosti „oprav ukazatelů“, nazývaných také thunks, když odlévání.
Zvažte následující kód C ++:
D *d = Nový D();B1 *b1 = d;B2 *b2 = d;
Zatímco d
a b1
po spuštění tohoto kódu ukáže na stejné místo v paměti, b2
ukáže na místo d + 8
(osm bajtů nad místem paměti d
). Tím pádem, b2
ukazuje na region v rámci d
že "vypadá jako" instance B2
, tj. má stejné rozložení paměti jako instance B2
.
Vyvolání
Volání na d-> f1 ()
je řešeno dereferencí d
je D :: B1
vpointer, vyhledávání f1
položka v tabulce virtuálních metod a poté dereferencování tohoto ukazatele k volání kódu.
V případě jednoduché dědičnosti (nebo v jazyce pouze s jedinou dědičností), pokud je vpointer vždy prvním prvkem v d
(stejně jako u mnoha překladačů) se to redukuje na následující pseudo-C ++:
(*((*d)[0]))(d)
Kde * d odkazuje na tabulku virtuálních metod z D a [0] odkazuje na první metodu v tabulce virtuálních metod. Parametr d se stává „tento“ ukazatel k objektu.
V obecnějším případě volání B1 :: f1 ()
nebo D :: f2 ()
je složitější:
(*(*(d[+0]/ * ukazatel na virtuální tabulku metod D (pro B1) * /)[0]))(d) / * Volejte d-> f1 () * /(*(*(d[+8]/ * ukazatel na tabulku virtuálních metod D (pro B2) * /)[0]))(d+8) / * Volejte d-> f2 () * /
Volání d-> f1 () předává ukazatel B1 jako parametr. Volání d-> f2 () předává ukazatel B2 jako parametr. Toto druhé volání vyžaduje opravu k vytvoření správného ukazatele. Umístění B2 :: f2 není v tabulce virtuálních metod pro D.
Pro srovnání, volání na d-> f0 ()
je mnohem jednodušší:
(*B1::f0)(d)
Účinnost
Virtuální volání vyžaduje ve srovnání s nevirtuálním voláním, které je jednoduše skokem na kompilovaný ukazatel, alespoň další indexovanou dereferenci a někdy doplnění „opravy“. Proto je volání virtuálních funkcí ze své podstaty pomalejší než volání nevirtuálních funkcí. Experiment provedený v roce 1996 naznačuje, že přibližně 6–13% času provádění se vynakládá pouze na odeslání do správné funkce, i když režie může dosahovat až 50%.[4] Cena virtuálních funkcí nemusí být u moderních tak vysoká procesor architektury díky mnohem větším mezipaměti a lepší predikce větve.
Dále v prostředích, kde Kompilace JIT se nepoužívá, volání virtuálních funkcí obvykle nemohou být podtrženo. V určitých případech může být možné, aby kompilátor provedl proces známý jako devirtualizace ve kterém jsou například vyhledávání a nepřímé volání nahrazeny podmíněným provedením každého vloženého textu, ale takové optimalizace nejsou běžné.
Aby se zabránilo této režii, kompilátoři se obvykle vyhýbají používání tabulek virtuálních metod, kdykoli lze hovor vyřešit na čas kompilace.
Tedy volání na f1
výše nemusí vyžadovat vyhledání tabulky, protože kompilátor to může zjistit d
může mít pouze a D
v tomto bodě a D
nepřepíše f1
. Nebo kompilátor (nebo optimalizátor) může být schopen zjistit, že neexistují žádné podtřídy B1
kdekoli v programu, který přepíše f1
. Volání na B1 :: f1
nebo B2 :: f2
pravděpodobně nebude vyžadovat vyhledávání v tabulce, protože implementace je specifikována výslovně (i když stále vyžaduje opravu tohoto ukazatele „this“).
Srovnání s alternativami
Tabulka virtuálních metod je obecně dobrý kompromis výkonu k dosažení dynamického odeslání, ale existují alternativy, jako například odeslání binárního stromu, s vyšším výkonem, ale různými náklady.[5]
Virtuální tabulky metod však umožňují pouze jediné odeslání na rozdíl od speciálního parametru „this“ hromadné odeslání (jako v CLOS nebo Dylan ), kde lze při dispečinku zohlednit typy všech parametrů.
Virtuální tabulky metod také fungují, pouze pokud je dispečink omezen na známou sadu metod, takže je lze umístit do jednoduchého pole vytvořeného v době kompilace, na rozdíl od kachní psaní jazyky (např Pokec, Krajta nebo JavaScript ).
Jazyky, které poskytují jednu nebo obě tyto funkce, se často odesílají vyhledáním řetězce v a hash tabulka nebo jinou rovnocennou metodou. Existuje celá řada technik, jak to urychlit (např. směna / názvy metod tokenizace, vyhledávání v mezipaměti, just-in-time kompilace ).
Viz také
Poznámky
- ^ G ++
-fdump-class-hierarchie
(počínaje verzí 8:-fdump-lang-class
) argument lze použít k výpisu tabulek virtuálních metod pro ruční kontrolu. Pro kompilátor AIX VisualAge XlC použijte-qdump_class_hierarchy
vypsat hierarchii tříd a rozložení tabulky virtuálních funkcí. - ^ https://stackoverflow.com/questions/17960917/why-there-are-two-virtual-destructor-in-the-virtual-table-and-where-is-address-o
Reference
- Margaret A. Ellis a Bjarne Stroustrup (1990) Annotated C ++ Reference Manual. Reading, MA: Addison-Wesley. (ISBN 0-201-51459-1)
- ^ Ellis & Stroustrup 1990, s. 227–232
- ^ Danny Kalev.„C ++ Reference Guide: The Object Model II“.2003. Nadpis „Dědičnost a polymorfismus“ a „Vícenásobná dědičnost“.
- ^ „C ++ ABI Closed Issues“. Archivovány od originálu dne 25. července 2011. Citováno 17. června 2011.CS1 maint: BOT: stav původní adresy URL neznámý (odkaz)
- ^ Driesen, Karel a Hölzle, Urs, „Přímé náklady na volání virtuálních funkcí v C ++“, OOPSLA 1996
- ^ Zendra, Olivier a Driesen, Karel, „Stresové testování řídicích struktur pro dynamický dispečink v Javě“, Str. 105–118, Proceedings of the USENIX 2nd Java Virtual Machine Research and Technology Symposium, 2002 (JVM '02)