programowanieProgramista C++

Czym jest tabela wirtualna (vtable) w C++? Jak kompilator realizuje polimorfizm dynamiczny i jakie są związane z tym szczegóły?

Zdaj rozmowy kwalifikacyjne z asystentem AI Hintsage

Odpowiedź.

W C++ polimorfizm dynamiczny jest realizowany przez mechanizm funkcji wirtualnych za pomocą specjalnej tabeli — vtable (tabela wirtualna). Dla każdej klasy z przynajmniej jedną funkcją wirtualną kompilator generuje vtable: jest to tablica wskaźników do funkcji wirtualnych klasy. Każdy obiekt takiej klasy przechowuje ukryty wskaźnik do vtable — tzw. vptr (wskaźnik wirtualny).

W momencie tworzenia obiektu klasy z funkcjami wirtualnymi vptr jest inicjowany na tabelę vtable, odpowiadającą klasie obiektu. Przy wywołaniu funkcji wirtualnej wywołanie nie następuje bezpośrednio, lecz przez adres przechowywany w vtable, wybierając odpowiednią implementację w zależności od rzeczywistego typu obiektu (nawet przy odniesieniu przez typ bazowy).

Przykład:

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(); // wywołanie przez vtable }

Wywołanie foo będzie odniesieniem do vtable: jeśli w Derived, to wywoła się nadpisana funkcja.

Szczegóły:

  • Jeśli klasa nie zawiera funkcji wirtualnych, vtable i vptr nie są dodawane.
  • Metody wirtualne nie stają się inlined, często tracąc wydajność.
  • Zwiększa to rozmiar instancji klasy (o rozmiar vptr — zazwyczaj 4/8 bajtów).
  • Wywołania wirtualne są wolniejsze od wywołań bezpośrednich.

Pytanie podchwytliwe.

"Czy metoda jest wirtualna, jeśli jest zadeklarowana jako virtual, ale nie została nadpisana w klasie pochodnej? Czy w takim przypadku wywołanie przez vtable będzie działać?"

Odpowiedź: Tak, jeśli funkcja jest zadeklarowana jako virtual w klasie bazowej, pozostaje wirtualna dla wszystkich potomków, nawet jeśli nie została nadpisana. Wywołanie przez wskaźnik bazowy w każdym przypadku będzie następować przez vtable. Jeśli metoda nie jest nadpisana — wywołana zostanie wersja z klasy bazowej.

Przykład:

struct Base { virtual void foo() { std::cout << "B "; } }; struct Derived : Base { }; Base* obj = new Derived(); obj->foo(); // Wyświetli "B", ale wywołanie przez vtable!

Przykłady rzeczywistych błędów z powodu nieznajomości szczegółów tematu.


Historia

W dużym projekcie zapomniano zadeklarować destruktor jako wirtualny w klasie bazowej z funkcjami wirtualnymi. Doprowadziło to do wycieków pamięci — przy usuwaniu przez wskaźnik bazowy wywoływany był tylko destruktor klasy bazowej, a nie pochodnej.


Historia

W klasie zadeklarowano metodę wirtualną, która przez pomyłkę została ukryta w dziedziczącym (nie override, a z inną sygnaturą/nazwą argumentów). Wywołania przez klasę bazową nie trafiały na odpowiednią funkcję, co prowadziło do niepoprawnego działania.


Historia

Aktualizacja vtable po wielodziedziczeniu doprowadziła do błędów: niewłaściwe wywołania metod przy użyciu dynamic_cast, ponieważ nie uwzględniano przesunięcia vptr w złożonej hierarchii klas.