Выбор 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, но расширяет его недостающими элементами Base2Vtable. С "макетом" я имею в виду, что указатель для 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();вызывает производный метод

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