VTables se koriste samo u slucaju da postoji nekakvo nasledjivanje. Dakle odmah covek moze da shvati da bilo kakvo nasledjivanje klasa odmah znaci (kakav-takav) overhead.
Uzmimo da imamo dve klase Mama i Dete:
Code:
class Mama {
public:
virtual void nesto() = 0;
virtual void ime() { printf("Mama\n"); }
Mama(int argGodina, int argDece = 1);
virtual ~Mama();
private:
int _godina;
int _kolikoDece;
};
class Dete {
public:
virtual void nesto();
Dete(int argGodina, bool jedino = false);
virtual ~Dete();
private:
bool _jedino;
};
Ovaj kod se otprilike u C-u "pise" ovako (imajte na umu da nije ceo kod u pitanju):
Code:
typedef int (*VirtualFunctionPointer)();
struct VTable {
int i;
int d;
VirtualFunctionPointer vtableFuncPtr;
};
struct Mama {
int _godina;
int _kolikoDece;
// pokazivac na vtable
VTable* vtablePtr;
};
struct Dete {
// Nasledjeno od Mama tipa:
int _godina;
int _kolikoDece;
// Clanovi dodani od strane "Dete" tipa:
bool _jedino;
// pokazivac na virtualnu funkciju
VTable* vtablePtr;
};
Kao sto se vidi svaka struktura koja predstavlja istoimenu klasu u prethodnom kodu poseduje odgovarajuce clanove i OBAVEZNO pokazivac na VTable...
U ovom konkretnom primeru, kako izgleda taj famozni VTable? Pokusacu da sto pribliznije (mada ni ja ne shvatam sve u potpunosti, tako da - ne uzimajte ovo zdravo-za-gotovo) objasnim:
Code:
VTable vtableMama[] = {
// nesto() je cisti virtuelni metod, te se ne sme direktno zvati
// zato pozovi pvc_error_handler() funkciju koja ce odmah da prijavi gresku.
{ 0, 0, pvc_error_handler },
// ime() metod
{ 0, 0, Mama_ime },
// ~Mama()
{ 0, 0, Mama_destructor}
};
VTable vtableDete[] = {
// nesto() je virtuelni, ali konkretni, metod
{ 0, 0, Dete_nesto },
// Poziva bazni ime() metod, jer u ovoj klasi nije definisan.
{ 0, 0, Mama_ime },
// ~Dete()
{ 0, 0, Dete_destructor}
};
Okej, imamo v-table. I sta sad?
Pa nista bez konstruktora naravno. :)
Code:
Mama* Mama_constructor(Mama* this, int argGodina, int argDece = 1)
{
if (this == 0) { // Ako memorija nije alocirana za strukturu Mama:
this = malloc(sizeof(Mama));
}
if (this) { // mozda malloc() ipak nije uspeo...
// inicijalizuj VTable pokazivac vtablePtr.
this->vtablePtr = vtableMama;
this->_godina = argGodina;
this->_kolikoDece = argDece;
}
return this;
}
void Mama_destructor(Mama* this, bool dynamic)
{
// Ponovo setujemo vtablePtr pointer, jer nije sigurno da on pokazuje na
// "dobru" vrednost.
/****
glavno telo destruktora
****/
this->vtablePtr = vtableMama;
// Oslobadjamo memoriju SAMO ako je memorija alocirana eksplicitno od strane Mama tipa
if (dynamic)
free(this);
}
Dete* Dete_constructor(Dete* this, int argGodina, int argDece, int argJedino = 0)
{
if (this == 0) { // Ako memorija nije alocirana za strukturu Dete:
this = malloc(sizeof(Dete));
}
if (this) { // mozda malloc() ipak nije uspeo...
// Pozovi prvo konstruktor bazne klase
Mama_constructor((Mama*)this, argGodina, argDece);
// inicijalizuj VTable pokazivac vtablePtr.
this->_jedino = argJedino;
}
return this;
}
void Dete_destructor(Dete* this, bool dynamic)
{
// Ponovo inicijalizujemo vtablePtr pointer, jer nije sigurno da on pokazuje na
// "dobru" vrednost.
this->vtablePtr = vtableDete;
/****
glavno telo destruktora
****/
// Pozivamo destriktor baznog tipa - Mama
Mama_destructor((Mama*) this, false);
// Oslobadjamo memoriju SAMO ako je memorija alocirana eksplicitno od strane Dete tipa
if (dynamic)
free(this);
}
Nema potrebe da pisem kod za ostale metode, ali ima potrebe da objasnim jos jednu stvar u vezi VTables, a to je - kako se koriste podaci iz njih. Tu dolazimo do main() funcije...
Code:
/* ukratko, ipak je 2.11AM ... */
int main()
{
// Kreiramo objekat tipa Dete, nema smisla kreirati objekat tipa Mama (apstraktna klasa).
// S obzirom da je prvi argument (this) NULL onda ce se memorija alocirati...
Mama* mamaPtr = Dete_constructor(NULL, 40, 2, true);
Dete objekatDete;
Dete_constructor(&objekatDete, 12, 0, 1);
// Pozovimo virtuelni metod nesto()
// Ovaj red je isto sto i: mamaPtr->nesto();
(mamaPtr->vtablePtr[0].vtableFuncPtr)(mamaPtr);
// Sledeci red je isto sto i mamaPtr->ime();
(mamaPtr->vtablePtr[1].vtableFuncPtr)(mamaPtr);
// Recimo da imamo metod imePrezime(str argIme, str argPrezime)
// i da imamo negde dve promenljive str1 i str2 tipa str (#define std::string str recimo :)
// I da je ova funkcija u VTables na mestu sa indeksom 5, onda bi je pozvali sa:
(mamaPtr->vtablePtr[5].vtableFuncPtr)(mamaPtr, "Marina", "Perazic");
// C++: delete objekatDete
Dete_destructor(&objekatDete, false);
return 0;
}
Dete_destructor linija treba da se pojasni... Naime, memorija je alocirana sa steka za "objekatDete", tako da nema smisla pozivati destructor sa argumentom true, jer ce ta varijabla svejedno da se obrise kada objekatDete izadje iz oblasti vazenja, u ovom slucaju kad se izadje iz funkcije main(). No, ovaj destruktor poziva destruktor bazne klase, te se tako sve lepo ocisti...
Igrom slucaja radim binding jedne C++ biblioteke za jedan maleni interpreter, te je trebalo da "flatten"-ujem C++ klase u C strukture, sa jos par zavrzlama, te otprilike znam kako interno C++ radi sa v-tables. Jos mnogo toga nije jasno, ali stvari lagano dolaze na svoje mesto i sve postaje jako jednostavno i prosto kada se ovo gore shvati.
Dejan Lekic
software engineer, MySQL/PgSQL DBA, sysadmin