Где переопределенный виртуальный метод, сохраненный в 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 для D
Vtable со всеми виртуальными функциями 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.