Где переопределенный виртуальный метод, сохраненный в vtable C++ в множественном наследовании

В C++ нет представления классов во время выполнения, но я всегда могу вызвать переопределенный виртуальный метод в производном классе. где этот переопределенный метод сохранен в vtable? Вот фрагмент кода для демонстрации:

struct B1 {
 virtual void f() { ... }
};
struct B2 {
 virtual void f() { ... }
 virtual void g() { ... }
};
struct D : B1, B2 {
 void f() { ... }
 virtual void h() { ... }
};

Какова схема памяти для объекта класса D? Где B1::f и B2::f сохранены в этой структуре памяти (если они вообще сохранены)?

3 ответа

Решение

Объект d класса D будет иметь только указатель на VMT класса D, который будет содержать указатель на D::f. Поскольку B1:f и B2::f могут вызываться только статически из области видимости класса D, в объекте нет необходимости d сохранить динамический указатель на эти переопределенные методы.

Эта причина не определена в стандарте, это просто обычная / логическая реализация компилятора.

На самом деле картина более сложная, поскольку VMT класса D включает в себя VMT классов B1 и B2. Но в любом случае, нет необходимости динамически вызывать B1::f, пока не будет создан объект класса B1.

Хотя в стандарте C++ ничего не предписано, во всех известных реализациях C++ используется один и тот же подход: у каждого класса хотя бы с виртуальной функцией есть vptr (указатель на vtable).

Вы не упомянули виртуальное наследование, которое представляет собой другое, более тонкое отношение наследования; не виртуальное наследование - это простое исключительное отношение между подобъектом базового класса и производным классом. Я буду предполагать, что все отношения наследования не являются виртуальными в этом ответе.

Здесь я предполагаю, что мы наследуем классы с хотя бы виртуальной функцией.

В случае одиночного наследования vptr из базового класса используется повторно. (Не повторное использование это просто тратит пространство и время выполнения.) Базовый класс называется "первичный базовый класс".

В случае множественного наследования макет производного класса содержит макет каждого базового класса, так же, как макет структуры в C содержит макет каждого члена. Макет D является B1 затем B2 (фактически в любом порядке, но порядок исходного кода обычно сохраняется).

Первый класс является основным базовым классом: в D вптр от B1 указывает на полный Vtable для DVtable со всеми виртуальными функциями D, Каждый vptr из неосновного базового класса указывает на вторичный vtable D: vtable только с виртуальными функциями из этого вторичного базового класса.

Конструктор D должен инициализировать каждый vptr экземпляра класса, чтобы указать на соответствующий vtable D,

Когда компилятор использует vtable-метод виртуальной диспетчеризации*, адрес переопределенной функции-члена сохраняется в vtable базового класса, в котором определена функция.

Каждый класс имеет доступ к vtables всех своих базовых классов. Эти таблицы хранятся вне макета памяти самого класса. Каждый класс с виртуальными функциями-членами, объявленными или унаследованными, имеет один указатель на свою собственную таблицу. Когда вы вызываете переопределенную функцию-член, вы указываете имя базового класса, функцию которого вы хотите вызвать. Компилятор знает о vtables всех классов, знает, как найти vtable вашего базового класса, выполняет поиск во время компиляции и напрямую вызывает функцию-член.

Вот короткий пример:

struct A {
    virtual void foo()   { cout << "A"; }
};
struct B : public A { }; // No overrides
struct C : public B {
    virtual void foo()   { cout << "C"; }
    void bar()           { B::foo(); }
};

Demo.

В приведенном выше примере компилятор должен искать B::foo, который не определен в классе B, Компилятор просматривает свою таблицу символов, чтобы выяснить, что B::foo реализуется в Aи генерирует вызов A::foo внутри C::bar,

* vtables не единственный метод реализации виртуальной диспетчеризации. Стандарт C++ не требует использования vtables.

Другие вопросы по тегам