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).
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.
"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!
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.