ProgrammatieC++ ontwikkelaar

Wat is een virtual table (vtable) in C++? Hoe implementeert de compiler dynamisch polymorfisme en welke nuances zijn verbonden met dit mechanisme?

Slaag voor sollicitatiegesprekken met de Hintsage AI-assistent

Antwoord.

In C++ wordt dynamisch polymorfisme geïmplementeerd via het mechanisme van virtuele functies met behulp van een speciale tabel — vtable (virtual table). Voor elke klasse met ten minste één virtuele functie genereert de compiler een vtable: dit is een array van pointers naar de virtuele functies van de klasse. Elk object van een dergelijke klasse slaat een verborgen pointer naar de vtable op — de zogenaamde vptr (virtual pointer).

Op het moment dat een object van een klasse met virtuele functies wordt gemaakt, wordt de vptr geïnitialiseerd naar de vtable die overeenkomt met de klasse van het object. Bij het aanroepen van een virtuele functie gebeurt de aanroep niet direct, maar via het adres dat in de vtable is opgeslagen, waarbij de juiste implementatie wordt gekozen afhankelijk van het echte type van het object (zelfs bij toegang via een basis type).

Voorbeeld:

class Base { public: virtual void foo() { std::cout << "Base::foo() "; } }; class Derived : public Base { public: void foo() override { std::cout << "Derived::foo() "; } }; void call_foo(Base* obj) { obj->foo(); // aanroep via vtable }

De aanroep foo zal een verwijzing naar de vtable zijn: als het in Derived is, zal de overschreven functie worden aangeroepen.

Nuances:

  • Als een klasse geen virtuele functies bevat, worden vtable en vptr niet toegevoegd.
  • Virtuele methoden worden vaak niet inlined, wat leidt tot prestatieverlies.
  • De grootte van de klasse-instantie neemt toe (afhankelijk van de grootte van vptr — meestal 4/8 bytes).
  • Virtuele aanroepen zijn trager dan directe aanroepen.

Strikvraag.

"Is een methode virtueel als deze is gedeclareerd als virtual, maar niet overschreven in de afgeleide klasse? Zal in dat geval de aanroep via vtable werken?"

Antwoord: Ja, als de functie is gedeclareerd als virtual in de basis klasse, blijft deze virtueel voor alle afstammelingen, zelfs als deze niet is overschreven. De aanroep via een basis pointer gaat in elk geval via de vtable. Als de methode niet is overschreven, zal de versie van de basis klasse worden aangeroepen.

Voorbeeld:

struct Base { virtual void foo() { std::cout << "B "; } }; struct Derived : Base { }; Base* obj = new Derived(); obj->foo(); // Geeft "B" terug, maar aanroep via vtable!

Voorbeelden van echte fouten als gevolg van onwetendheid over de nuances van het onderwerp.


Verhaal

In een groot project was vergeten de destructor als virtueel te verklaren in de basis klasse met virtuele functies. Dit leidde tot geheugenlekken — bij verwijdering via een basis pointer werd alleen de destructor van de basis klasse aangeroepen, niet die van de afgeleide.


Verhaal

In de klasse was een virtuele methode gedeclareerd die per ongeluk was verborgen in de afgeleide (niet override, maar met een andere handtekening/argumenten naam). Aanroepen via de basis klasse kwamen niet bij de juiste functie, wat leidde tot onjuiste werking.


Verhaal

Actualisering van vtable na meervoudige overerving leidde tot fouten: onjuiste aanroepen van methoden bij gebruik van dynamic_cast, omdat de verschuiving van vptr in een complexe klassehiërarchie niet in aanmerking werd genomen.