Выбор vptr в случае множественного наследования
Это похоже на многие предыдущие вопросы, но спрашивает что-то, на что я не смог найти ответ.
#include <iostream>
using namespace std;
class Base1 {
public:
int b1_data;
virtual void b1_fn() {cout << "I am b1\n";}
};
class Base2 {
public:
int b2_data;
virtual void b2_fn() {cout << "I am b2\n";}
};
class Derived : public Base1, public Base2 {
public:
int d_data;
void b1_fn() {cout << "I am b1 of d\n";}
void b2_fn() {cout << "I am b2 of d\n";}
};
int main() {
Derived *d = new Derived();
Base1 *b1 = d;
/*My observation mentioned below is implementation dependant, for learning,
I assume, there is vtable for each class containing virtual function and in
case of multiple inheritance, there are multiple vtables based on number of
base classes(hence that many vptr in derived object mem layout)*/
b1->b1_fn(); // invokes b1_fn of Derived because d points to
// start of d's memory layout and hence finds vtpr to
// Derived vtable for Base1(this is understood)
Base2 *b2 = d;
b2->b2_fn(); // invokes b2_fn of Derived but how? I know that it "somehow"
// gets the offset added to d to point to corresponding Base2
// type layout(which has vptr pointing to Derived vtable for
// Base2) present in d's memory layout.
return 0;
}
В частности, как b2 указывает на vptr для vtable из Derived для Base2, чтобы добраться до b2_fn()? Я пытался увидеть memlayout dump из gcc, но не смог разобраться.
2 ответа
Компилятор в случае множественного наследования создает его таблицы так, чтобы у каждого подобъекта была соответствующая таблица. Конечно, это зависит от реализации (как и сами таблицы), но это будет организовано так:
Base1
объект имеет vptr, указывающий на виртуальную таблицу, содержащую уникальный указатель наBase1::b1_fn
Base2
объект имеет vptr, указывающий на виртуальную таблицу, содержащую уникальный указатель наBase2::b2_fn
Derived
объект имеет vptr, указывающий на vtable, который начинается с макета vtable, соответствующегоBase1
, но расширяет его недостающими элементамиBase2
Vtable. С "макетом" я имею в виду, что указатель дляb1_fn()
находится в том же смещении, но это может указывать на переопределяющую функцию. Итак, здесь таблица будет содержатьDerived::b1_fn
с последующимDerived::b2_fn
, Этот комбинированный макет гарантирует, чтоBase1
подобъект вDerived
может поделиться виртуальной таблицей со своим ребенком.- Но
Derived
Объект состоит из 2 подобъектов:Base1
Субъект будет сопровождатьсяBase2
подобъект, который будет иметь свой собственный vtable, используя макет, необходимый дляBase2
, но снова сBase2::b2_fn
вместо оригинального.
При кастинге Derived
указатель на Base2
указатель, компилятор будет указывать на Base2
подобъект с его vtable, применяя фиксированное смещение, определенное во время компиляции.
Между прочим, если бы вы делали downcasting, компилятор также использовал бы фиксированное смещение в другом направлении, чтобы найти начало Derived
, Это все довольно просто, пока вы не используете виртуальные базы, в которых техника фиксированного смещения больше не может работать. Виртуальные базовые указатели должны затем использоваться, как объяснено в этом другом ответе SO
Эта статья доктора Добба - лучшее объяснение всех этих макетов с некоторыми хорошими картинками.
#include <iostream>
using namespace std;
class Base1 {
public:
virtual void b1_fn() {cout << "I am b1\n";}
};
class Base2 {
public:
virtual void b2_fn() {cout << "I am b2\n";}
};
class Derived : public Base1, public Base2 {
public:
void b1_fn() {cout << "I am b1 of d\n";}
void b2_fn() {cout << "I am b2 of d\n";}
};
int main() {
Base1 b1;
Base2 b2;
Derived d;
cout<<"size of base1 object"<<sizeof(b1)<<endl;
cout<<"size of base2 object"<<sizeof(b2)<<endl;
cout<<"size of derived object"<<sizeof(d)<<endl;
return 0;
}
Вывод вышеуказанной программы: 8 8 16
Итак, вы можете видеть, что производный объект унаследовал два vptr от base1 и base2.
оба vptr в производном классе указывают на vtable производного класса. поскольку вы переопределили базовые методы в производном классе, поэтому vptr будет указывать на методы производного класса.
Если вы не переопределили методы в производном классе, тогда vtable будет иметь адрес методов базового класса, а vptr будет указывать на эти методы.
Вот почему b2->b2_fn();вызывает производный метод