#опытным
С++ давно ушел от компиляции в С. Но таблицы виртуальных функций, как средство реализации динамического полиморфизма, остались в компиляторах.
Как устроены vtables сейчас, когда С++ напрямую компилируется в ассемблер?
Идея все та же:
👉🏿 На каждый полиморфный класс создается своя vtable
👉🏿 Она представляет собой просто массив адресов. В ассемблере адреса нетипизированные, поэтому они просто лежат там чиселками.
👉🏿 В каждом объекте хранится vptr - указатель на эту таблицу.
👉🏿 vtpr инициализируется в самом базовом классе его адресом его vtable и затем каждый конструктор наследника присваивает в него адрес своей таблицы
Реализации вносят свои особенности, но это ядро остается неизменным.
Вот примерчик(можете смотреть на годболте):
class Person {
protected:
std::string name;
int age;
public:
Person(const std::string &n, int a) : name(n), age(a) {}
virtual std::string getRole() const { return "Person"; }
virtual std::string getName() const { return name; }
virtual void describe() const {
std::cout << name << " (" << age << " years)";
}
virtual ~Person() = default;
};
class Employee : public Person {
std::string position;
public:
Employee(const std::string &n, int a, const std::string &pos)
: Person(n, a), position(pos) {}
std::string getRole() const override { return "Employee"; }
void describe() const override {
std::cout << name << " (" << age << " years) - " << position;
}
};
int main() {
Person * p = new Employee("Steven", 42, "CEO");
p->describe();
std::cout << p->getName() << std::endl;
}Будем сейчас разбирать gcc-шный асм.
Вот так выглядят таблицы для обоих классов:
vtable for Person:
.quad 0
.quad typeinfo for Person
.quad Person::getRoleabi:cxx11 const
.quad Person::getNameabi:cxx11 const
.quad Person::describe() const
.quad Person::~Person()
vtable for Employee:
.quad 0
.quad typeinfo for Employee
.quad Employee::getRoleabi:cxx11 const
.quad Person::getNameabi:cxx11 const
.quad Employee::describe() const
.quad Employee::~Employee()
quad - это 64-битное число. Видно, что таблицы - это статические массивы.
Первым числом у них является offset to top, это число нужно для корректной работы множественного наследования, не будем вдаваться в детали.
Второй число - адрес расположения информации о динамическом типе(RTTI) объекта. Эта информация нужна, например, для dynamic_cast'а.
Дальше расположены адреса виртуальных функций нужных классов. Заметьте, что адреса расположены в порядке объявления виртуального метода в классе. Если в дочернем классе переопределяется метод, то в его vtable указатель родительского метода заменяется на переопределенный(методы
getRole и describe). Если наследник не переопределяет метод, то в таблице остается указатель на родительский метод(getName).В конструкторе Employee происходит такое:
; Сначала вызывается конструктор Person (устанавливает vptr Person)
call Person::Person(...) [base object constructor]
; Затем перезаписывается vptr на таблицу Employee
mov edx, OFFSET FLAT:vtable for Employee+16
mov rax, QWORD PTR [rbp-24]
mov QWORD PTR [rax], rdx
Вызывается конструктор базового класса и сразу же после этого переприсваивается vptr на таблицу класса Employee.
Ну а виртуальный вызов выглядит просто как call нужного адреса:
mov rax, QWORD PTR [rbp-40] ; Загружаем указатель p
mov rax, QWORD PTR [rax] ; Загружаем vptr (указывает на vtable+16)
add rax, 16 ; Смещаемся к 4-му слоту (describe)
mov rdx, QWORD PTR [rax] ; Загружаем адрес функции describe
mov rax, QWORD PTR [rbp-40] ; Загружаем this (указатель p)
mov rdi, rax ; Передаем this как первый параметр
call rdx ; Виртуальный вызов describe