Virtuel metodetabel ( VMT) - en koordineringstabel eller vtabel - en mekanisme, der bruges i programmeringssprog til at understøtte dynamisk matchning (eller sen bindingsmetode).
Lad os sige, at et program indeholder flere klasser i et arvehierarki: en basisklasse Cat og to underklasser DomesticCat og Lion. Klassen Catdefinerer en virtuel funktion speak , så dens underklasser kan levere den passende implementering (dvs. "miav" eller "brøl").
Når et program kalder en metode speakpå en pointer Cat(som kan pege på en klasse Cateller en hvilken som helst underklasse Cat), skal kontekstmiljøet (runtime-miljøet) være i stand til at bestemme, hvilken implementering der kaldes, afhængigt af den aktuelle type af det spidse objekt.
Der er mange forskellige måder at implementere dynamiske links på som denne, men den virtuelle tabelløsning er ret almindelig i C++ og relaterede sprog (såsom D og C# ). Sprog, der har en adskillelse mellem et objekts API og dets implementering, som Visual Basic og Delphi , har også en tendens til at bruge virtuelle tabelanaloger, da dette tillader objekter at bruge en anden implementering blot ved at bruge et andet sæt metodepointere.
Et objekts koordineringstabel indeholder adresserne på objektets dynamisk forbundne metoder. Metoden kaldes, når metodens adresse hentes fra tabellen. Koordineringstabellen vil være den samme for alle objekter, der tilhører samme klasse, så deling er tilladt. Objekter, der tilhører typekompatible klasser (for eksempel dem, der er på samme niveau i nedarvningshierarkiet) vil have lignende koordineringstabeller: adressen på en given metode vil blive fastsat med samme offset for alle typekompatible klasser. Ved at vælge en metodes adresse fra den givne koordineringstabel med en forskydning får vi den metode, der er knyttet til den aktuelle klasse af objektet. [en]
C++-standarderne definerer ikke klart, hvordan dynamisk koordinering skal implementeres, men compilere bruger ofte en eller anden variation af den samme grundmodel.
Normalt opretter compileren en separat virtuel tabel for hver klasse. Efter at objektet er oprettet, tilføjes en pointer til den virtuelle tabel, kaldet en virtuel tabel pointer eller vpointer (også nogle gange kaldet en vptr eller vfptr), som et skjult medlem af det objekt (og ofte som det første medlem). Compileren genererer også "skjult" kode i konstruktøren af hver klasse for at initialisere dens objekts vpointere med adresserne på den tilsvarende vtable.
Overvej følgende klasseerklæringer i C++:
klasse B1 { offentligt : ugyldig f0 () {} virtuelt tomrum f1 () {} int int_in_b1 ; }; klasse B2 { offentligt : virtuelt tomrum f2 () {} int int_in_b2 ; };bruge til at oprette følgende klasse:
klasse D : offentlig B1 , offentlig B2 { offentligt : ugyldig d () {} void f2 () {} // tilsidesætte B2::f2() int int_in_d ; };og følgende C++ kodestykke:
B2 * b2 = ny B2 (); D * d = nyt D ();G++ 3.4.6 fra GCC -pakken opretter følgende 32-bit hukommelseskort til objektet b2 (здесь и далее ТВМ - таблица виртуальных методов): [nb 1]
b2: +0: pointer til TVM B2 +4: int_in_b2 værdi TVM B2: +0: B2::f2()og for objektet dvil hukommelsesskemaet være sådan:
d: +0: pegepind til TVM D (for B1) +4: int_in_b1 værdi +8: pointer til TVM D (for B2) +12: int_in_b2 værdi +16: int_in_d værdi Samlet størrelse: 20 bytes. TVM D (for B1): +0: B1::f1() // B1::f1() er ikke omdefineret TVM D (for B2): +0: D::f2() // B2::f2() erstattet af D::f2()Det skal bemærkes, at ikke-virtuelle funktioner (såsom f0) generelt ikke kan vises i en virtuel tabel, men der er undtagelser i nogle tilfælde (såsom standardkonstruktøren).
Omdefinering af en metode f2()i en klasse Dimplementeres ved at duplikere TCM B2og erstatte markøren til med en B2::f2()markør til D::f2().
Multipel nedarvning af klasser til B1og B2fra klassen Dved hjælp af to virtuelle metodetabeller, en for hver basisklasse. (Der er andre måder at implementere multiple arv, men dette er den mest almindelige). Dette resulterer i behovet for " pointers to address record " (bindinger) ved oprettelse.
Overvej følgende C++-kode:
D * d = nyt D (); B1 * b1 = dynamic_cast < B1 *> ( d ); B2 * b2 = dynamic_cast < B2 *> ( d );Mens dog b1peger på ét sted i hukommelsen efter udførelse af denne kode, b2vil pege på en hukommelsesplacering d+8(en forskydning på otte bytes fra placering d). Således b2peger på et område af hukommelsen inden for d, som "ligner" en entitet B2, dvs. har samme hukommelseslayout som entiteten B2.
Kaldet d->f1()opstår, når vpointeren er derefereneret D::B1fra d: at slå op i o-indgangen f1i den virtuelle tabel, og derefter dereferencere denne pointer kalder koden.
I tilfælde af enkelt nedarvning (eller i tilfælde af et sprog, der kun understøtter enkelt arv), hvis vpointer altid er det første element i d(som det er tilfældet med mange compilere), så løses dette med følgende pseudo-C++ kode :
* (( * d )[ 0 ])( d )I et mere generelt tilfælde, som nævnt ovenfor, vil det være vanskeligere at ringe f1(), D::f2()og B2::f2()videred
* (( d -> /*TBM pointer D (for B1)*/ )[ 0 ])( d ) // d->f1(); * (( d -> /*TBM pointer D (for B2)*/ )[ 0 ])( d + 8 ) // d->f2(); * (( /* adresse på TVM B2 */ )[ 0 ])( d + 8 ) // d->B2::f2();Til sammenligning er opkaldet d->f0()meget enklere:
* B1 :: f0 ( d )Et virtuelt opkald kræver mindst en ekstra indekseret dereference, og nogle gange en ekstra "fixup" svarende til et ikke-virtuelt opkald, som er et simpelt spring til en kompileret pointer. Derfor er det i sagens natur langsommere at kalde virtuelle funktioner end at kalde ikke-virtuelle. Et eksperiment udført i 1996 viste, at cirka 6-13 % af udførelsestiden bruges på blot at søge efter den passende funktion, mens den samlede stigning i eksekveringstiden kan nå op på 50 % [2] . Omkostningerne ved at bruge virtuelle funktioner på moderne processorarkitekturer er muligvis ikke så høje på grund af tilstedeværelsen af betydeligt større caches og bedre grenforudsigelse .
I et miljø, hvor JIT -kompilering ikke bruges, kan virtuelle funktionskald normalt ikke være interne . Selvom det er muligt for compileren at erstatte opslag og indirekte invokation, for eksempel ved betinget at udføre hver intern krop, er en sådan optimering ikke almindelig.
For at undgå sådant spild undgår kompilatorer normalt at bruge virtuelle tabeller, når der kan foretages et opkald på kompileringstidspunktet.
Således kræver ovenstående kald f1muligvis ikke et opslag af den virtuelle tabel, da compileren kun kan rapportere, hvad den dkan have på det tidspunkt D, i stedet Dfor at omdefinere f1. Eller compileren (eller alternativt optimeringsværktøjet) kan muligvis opdage fraværet af underklasser B1i programmet, der tilsidesætter f1. Opkald B1::f1eller B2::f2vil sandsynligvis ikke kræve et opslag af den virtuelle tabel på grund af den eksplicitte implementering (selvom binding til 'denne' pointer stadig er påkrævet).
Den virtuelle tabel ofrer generelt ydeevne for at opnå dynamisk udvælgelse, men der er mange alternativer til det, såsom binært træudvælgelse, som har bedre ydeevne, men forskellige eksekveringshastigheder [3] .
Den virtuelle tabel leveres dog kun til enkelt afsendelse på den specielle "dette" parameter, i modsætning til multipel afsendelse (som i CLOS eller Dylan ), hvor typerne af alle parametre kan tildeles under afsendelse.
En virtuel tabel fungerer også kun, hvis afsendelsen er begrænset til et kendt sæt af metoder, så mange virtuelle tabeller kan sættes i et simpelt array på kompileringstidspunktet, i modsætning til sprog, der understøtter duck typing (såsom Smalltalk , Python eller JavaScript ).
Sprog, der understøtter en eller begge af disse muligheder, sendes ofte ved at slå en streng op i en hash-tabel eller en anden tilsvarende metode. Der er en del tricks til at forbedre hastigheden (f.eks. tokenisering af metodenavne, anvendelse af caching, JIT - kompilering), og afsendelsestid har ofte ikke en væsentlig indvirkning på den samlede behandlingstid, men på trods af dette er virtuelle tabelopslag mærkbart hurtigere . . En virtuel tabel er også nemmere at implementere og fejlfinde, og er også tættere på "C filosofi" end string hash tables link? .