fel
le

Késői kötés háttere

A késői kötés során csak futás közben dől el, hogy a virtuális metódus meghívása konkrétan melyik metódus legyen. Ennek során felhasználásra kerül a híváshoz felhasznált példány típusa.

A háttérben futó módszer ennél kifinomultabb. Egy táblázatra alapozza a működését, amelyet VMT táblának, Virtuális Metódus Tábla nevezünk.

A Virtuális Metódus Tábla

A VMT táblát a fordítóprogram készíti fordítás közben. A VMT táblának annyi bejegyzése van, ahány virtuális metódusa az adott osztálynak van. A VMT tábla osztályhoz rendelt, minden osztálynak saját VMT-je van.

A VMT tábla készítésének szabályai egy konkrét osztály esetén:

  • Ha nincs ős osztály, akkor a VMT tábla induláskor üres
  • Ha van ős osztály, akkor a VMT tábla induláskor legyen az ős osztály VMT táblájának másolata
  • ha jelen osztályban bevezetnek egy új virtuális metódust a 'virtual' kulcsszó segítségével, akkor a VMT tábla végére új sor adódik, ezen metódus azonosítójával
  • ha a jelen osztályban valamely, már korábban bevezetett virtuális metódust felüldefiniálnak az 'override' kulcsszóval, akkor ezen metódus sorát jelölő VMT táblabeli sorban frissíteni kell a metódus azonosítóját az újra

Példa:

class TRepulo
{
   virtual public void Felszall()  { ... }
   virtual public void Leszall()   { ... }
   virtual public void Emelkedik()  { ... }
   public void Repul()     { ... }
   
   public void Gyakorlokor()
   {
     Felszall();
     Repul();
     Leszall();
   }
}
 

Ezen osztály VMT táblája az alábbiak szerint néz ki:

TRepulo osztály VMT
----------------------------------
Felszall  |  TRepulo->Felszall()
Leszall   |  TRepulo->Leszall()
Emelkedik |  TRepulo->Emelkedik()
 

Vagyis szerepel benne, hogy ha futás közben a 'Leszall()' metódust szeretnénk meghívni, akkor a TRepulo osztálybeli példányok esetén ezen metódusból a legfrisseb változat a TRepulo osztályban definiált metódus.

class THelikopter:TRepulo
{
   override public void Felszall() { ... }
   virtual public void Lassit() { ... }
   override public void Leszall() { ... }
}
 

Ezen gyermekosztály VMT táblája az alábbiak szerint néz ki:

THelikopter osztály VMT
---------------------------------------
Felszall   | THelikopter->Felszall()
Leszall    | THelikopter->Leszall()
Emelkedik  | TRepulo->Emelkedik()
Lassit     | THelikopter->Lassit()
 

VMT tábla használata

Amikor példányosítunk, a kiválasztott konstruktor dönti el hogy melyik osztály VMT tábláját csatolja a példányhoz hozzá. Ennek során az adott osztály memóriában eleve betöltött VMT táblájának memóriacímét egy speciális mezőben a példány memóriaterületén tárolja. Nevezzük ezt a kódban egyébként nem elérhető mezőt VMT-nek az egyszerűség kedvéért.

Tehát a konstruktor feltölti a példány VMT mezőjét annak az osztálynak a VMT táblájára mutató memóriacímmel, amelyben őt, mint konstruktor definiáltuk. Amikor a program fut, és eléri azt a pontot, ahol a késői kötés szerepel, akkor a generált kód egyszerűen kiolvassa az aktuális példány VMT mezőjét, és az ott szereplő táblázatban "megkeresi" a kívánt bejegyzést, és kiolvassa, hogy melyik metódus-változatot kell meghívni.

TRepulo f15 = new TRepulo();
f15.Gyakorlokor();
 

Az f15 példány a TRepulo() konstruktor használata mellett lett példányosítva, vagyis az f15.VMT mező a TRepulo osztály VMT táblázatát azonosítja.

Az f15.Gyakorlokor() hívás során:

{
 Felszall()  -> ?? késői kötés ??
 VMT[ 'Felszall' ]  ==>  TRepulo->Felszall()
 // így meghívásra kerül a TRepulo osztályban definiált Felszall() metódus
 
THelikopter apache = new THelikopter();
apache.Gyakorlokor();
 

Ezzel szemben az apache példány a THelikopter() konstruktor példányosította, így a VMT mezőben a THelikopter VMT táblázatának címe került.

Az f15.Gyakorlokor() hívás során:

{
 Felszall()  -> ?? késői kötés ??
 VMT[ 'Felszall' ]  ==>  THelikopter->Felszall()
 // így meghívásra kerül a THelikopter osztályban definiált Felszall() metódus
 

Figyeljük meg, hogy mindkét lépésben igazából ugyanaz a lépéssorozat hajtódik végre a késői kötés feloldása során, és csak a VMT mező értéke jelenti a különbséget.

VMT tábla memóriaigénye

Először is vegyük észre, hogy a VMT tábla készítésének mechanizmusa garantálja, hogy egy metódus a gyerekosztályok VMT táblájában is mindíg ugyanabban a sorban szerepel. Ezért a táblázatban igazából nem kell keresni, hiszen fordítás során a fordító (aki a VMT táblákat is készíti) eleve tudni fogja a késői kötés során majd hogy a keresett metódus melyik sorban fog majd szerepelni:

{
  Felszall()  -> ?? késői kötés ??
  // a Felszall() metódusok a 0. sorban vannak
  VMT[ 0 ]  ==>  TRepulo->Felszall()
  // így meghívásra kerül a TRepulo osztályban definiált Felszall() metódus
 

Ennek megfelelően a VMT tábla maga nem tárolja a metódusok neveit, valójában csak a második oszlopok szerepelnek benne. A második oszlopban pedig a szóban forgó metódusok memóriacímei szerepelnek. Ezen memóriacímek segítségével lehet a metódust meghívni - egy egyszerű ugró utasítással.

A memóriacímek a 32 bites processzorok esetén mindössze 4 byte-on tárolódnak, ezért egy VMT bejegyzés 4 byte. Ahány bejegyzés van, annyiszor 4 byte a helyigénye a VMT táblának.

Ugyanakkor vegyük észre, hogy a gyermekosztályok VMT táblája legalább annyi sort tartalmaz, mint ahány sora az ős osztályénak volt, hiszen a gyermekosztály VMT-jének készítése azzal kezdődik, hogy lemásoljuk az ős osztály VMT-jét.

Ezért a VMT táblák a fejlesztési láncon lefelé haladva egyre hosszabbak és hosszabbak. Akkor is, ha a gyermekosztályok egyébként nem módosítanak akár egyetlen virtuális metódust sem. Ezért nem mondható el róla, hogy ezen megoldás memóriatakarékos lenne.

Másrészről ezen készítési metódus garantálja, hogy a metódusok a gyermekosztályok VMT tábláiban is minden esetben ugyanazon sorban szerepeljenek. Ezért a VMT táblázatokban sosem zajlik keresés a futás során, célirányosan az adott sor kiolvasása után a kiválasztott metódus máris meghívható. Ezért a VMT táblával működő késői kötés valójában aránylag gyorsan mondható.

A Dinamikus Metódus Tábla

A DMT szerepe hasonló, mint a VMT-é, a késői kötés támogatása. A DMT tábla felépítése is nagyon hasonló, egy jelentős különbséggel: a gyermekosztályok DMT táblájában csak a módosítások szerepelnek.

A VMT tábla készítésének szabályai egy konkrét osztály esetén:

  • Tároljuk el az ős osztályunk DMT táblájának címét
  • A DMT tábla induláskor mindenképpen legyen üres, függetlenül attól, hogy van-e ős osztályunk vagy nincs
  • ha jelen osztályban bevezetnek egy új virtuális metódust a virtual kulcsszó

segítségével, akkor a DMT táblához új sor adódik, ezen metódus azonosítójával

  • ha a jelen osztályban valamely, már korábban bevezetett virtuális metódust felüldefiniálnak az override kulcsszóval, akkor a DMT táblához új sor adódik, ezen metódus azonosítójával

Ennek megfelelően a fenti osztályok esetén a DMT táblák az alábbiak szerint néz ki:

TRepulo osztály DMT
--------------------------------
ŐS osztály DMT  | -- nincs --
Felszall        | TRepulo->Felszall()
Leszall         | TRepulo->Leszall()
Emelkedik       | TRepulo->Emelkedik()
 

Valamint:

THelikopter osztály DMT
-----------------------
ŐS osztály DMT  | TRepulo DMT
Felszall        | THelikopter->Felszall()
Leszall         | THelikopter->Leszall()
Lassit          | THelikopter->Lassit()
 

Vegyük észre, hogy a DMT táblákban nem ugyanabban a sorban szerepelnek a metódusok. Ez két fontos következménnyel jár:

  • a DMT táblabeli bejegyzésekhez ténylegesen tárolni kell, melyik metódushoz tartozik, hiszen ez egyéb módon nem kideríthető. Nyilván nem a metódusok neve van tárolva, mert az felettéb sok memóriát kötne le. Helyette a metódusokhoz azonosítókat generál a fordító, melyek egész számok. Ezek megvalósítástól függően 2 vagy 4 byte-os azonosítók, és a DMT táblában minden egyes bejegyzéshez hozzátartozik a megfelelő metódus memóriacím mellé. Ezért minden DMT beli sor nem 4, hanem 6 vagy 8 byte.
  • a DMT táblában a késői kötés feloldásának pillanatában valóban keresni kell, hiszen nem tudhatja a rendszer, hogy melyik sorban fogja a metódus címét megtalálni. Ezen keresés mindenképpen időt felemésztő tevékenység, ezért a DMT tábla használata sokkal lassabb,mint a VMT-é. A keresés során előfordulhat, hogy nem is ebben a táblázatban szerepel a keresett metódus, hanem az ősében. Ezért is kell tárolni az ős DMT-jét, mert elképzelhető, hogy keresni kell majd benne.

Mivel a DMT táblabejegyzés több memóriát köt le, mint a VMT egy bejegyzése, ezért csak akkor köt le a DMT kevesebb memóriát, ha sokkal jellemzőbb az, hogy a gyermekosztályokban nem definiáljuk felül a virtuális metódusokat. Ellenkező esetben könnyen adódhat az is akár, hogy a DMT táblás megoldás több memóriánkba kerül, mint a VMT-s megoldás. Ugyanakkor a DMT-s megoldás biztosan lassúbb, mint a VMT-s megoldás.

Bizonyos nyelveken, mint például a Delphi, a programozónak van lehetősége dönteni, melyik metódust melyik rendszer szerint kezelje a fordító. Ekkor az alábbi szempontokat javasoljuk a döntés meghozatalához:

  • amennyiben a metódust a gyermekosztályokban nagy valószínűséggel felül fogják definiálni, úgy tegyük a VMT rendszerbe, hiszen ez esetben így tudunk memóriát spórolni.
  • amennyiben a metódust gyakran hívják majd meg, esetleg egy ciklus belsejében, úgy szintén tegyük a VMT rendszerbe, mert ennek kezelési sebessége jelentősen hatékonyabb.
  • egyéb esetekben szóba kerülhet a DMT is.

Mivel a manapság nincs jelentős ára a memóriának, ezért nincs különösebb okunk hogy a memóriaterületeken byte-os aprólékossággal spórolni, ezért az újabb programozási nyelveken már a DMT-s technológia nincs használatban. Sőt. A JAVA nyelvben már minden metódus eleve Virtuálisnak minősül, és a felüldefiniálása pedig eleve Override-nak. Itt minden kötés mindíg késői lesz. Ez egy szempontból korrekt, hiszen nehezebb hibát véteni. Ugyanakor a korai kötés azon túl hogy nem köt le extra memóriát, a lehető leggyorsabb működésű kötés is. Ennek előnyeit ki kell tudnunk használni.

Hernyák Zoltán
2013-03-17 19:17:32