Ромбовидное полиморфное наследование: sizeof Самый производный класс

Я понимаю, что Ромбовидное наследование вызывает неоднозначность и его можно избежать, используя наследование через virtual Base Classes, вопрос не в этом. Вопрос заключается в размере самого производного класса в ромбовидной иерархии, когда классы полиморфны. Вот пример кода и пример вывода:

#include<iostream>

using namespace std;

class Base
{
    public:
        virtual void doSomething(){}  
};

class Derived1:public virtual Base
{
    public:
       virtual void doSomething(){}
};

class Derived2:public virtual Base
{
    public:
       virtual void doSomething(){}
};

class Derived3:public Derived1,public Derived2
{
    public:
       virtual void doSomething(){}
};

int main()
{
    Base obj;
    Derived1 objDerived1;
    Derived2 objDerived2;
    Derived3 objDerived3;

    cout<<"\n Size of Base: "<<sizeof(obj);
    cout<<"\n Size of Derived1: "<<sizeof(objDerived1);
    cout<<"\n Size of Derived2: "<<sizeof(objDerived2);
    cout<<"\n Size of Derived3: "<<sizeof(objDerived3);

    return 0;
}

Результат, который я получаю:

 Size of Base: 4
 Size of Derived1: 4
 Size of Derived2: 4
 Size of Derived3: 8

Насколько я понимаю, Base содержит виртуальную функцию-член и, следовательно,
sizeof Base = размер vptr = 4 в этой среде.

Аналогично в случае классов Derived1 и Derived2.

Вот мои вопросы, связанные с описанным выше сценарием:
Как насчет размера объекта класса Derived3? Означает ли это, что класс Derived3 имеет 2 vptr?
Как класс Derived3 работает с этими 2 vptr, есть идеи о механизме этого использует?
Классы sizeof оставлены как деталь реализации компилятора и не определены стандартом (поскольку сам виртуальный механизм является деталью реализации компиляторов)?


person Alok Save    schedule 31.03.2011    source источник
comment
относительно стандартного вопроса. Да, механизм реализации виртуальных методов является деталями реализации и не указан. Да, фактический результат sizeof также является деталями реализации, он особенно зависит от размера указателя, если бы вы работали на 64-битной платформе, вы бы увидели 8/8/8/16.   -  person Matthieu M.    schedule 31.03.2011


Ответы (4)


Да, Derived3 имеет два указателя vtable. Если вы обращаетесь к нему по значению, он использует версию Derived3, или выбирает функцию из родителя, или обозначает, что он неоднозначен, если не может принять решение.

В случае дочернего элемента он использует виртуальную таблицу, соответствующую родительской 1/2, которая используется полиморфно.

Обратите внимание, что вы неправильно использовали виртуальное наследование: я считаю, что Derived1 и 2 должны фактически наследоваться от Base. sizeof(Derived3) по-прежнему кажется 8, потому что у него все еще есть два возможных родителя, которые можно рассматривать как Derived3. Когда вы выполняете приведение к одному из родителей, компилятор фактически настраивает указатель объекта, чтобы иметь правильную виртуальную таблицу.

Также я должен отметить, что все, что связано с vtable, зависит от реализации, потому что в стандарте нет даже упоминания о vtables.

person Mark B    schedule 31.03.2011
comment
Спасибо, я исправил опечатку Derived1 и Derived2, фактически наследующую от Base. О/П остается прежним. - person Alok Save; 31.03.2011

Небольшое исправление вашего кода: виртуальный должен быть в определении производного2 и производного 3, чтобы работать.

http://www.parashift.com/c++-faq-lite/multiple-inheritance.html#faq-25.9

person ManicQin    schedule 31.03.2011

Я думаю, вы задаетесь вопросом о чем-то, что полностью зависит от реализации. Вы не должны ничего предполагать о размере классов.

Редактировать: хотя любопытство — проверенное качество ;-)

person Stephane Rolland    schedule 31.03.2011

Рассмотрим немного другой случай:

struct B { virtual void f(); };
struct L : virtual B { virtual void g(); };
struct R : virtual B { virtual void h(); };
struct D : L, R {};

В типичной реализации L::g будет находиться в той же позиции (скажем, с индексом 0) в vtable L, что и R:h в vtable R. Теперь рассмотрим, что происходит с данным кодом:

D* pd = new D;
L* pl = pd;
R* pr = pd;
pl->g();
pr->h();

В последних двух строках компилятор сгенерирует код для поиска адреса функции в той же позиции в виртуальной таблице. Таким образом, vtable, доступ к которой осуществляется через pl, не может совпадать с таблицей (или ее префиксом), доступ к которой осуществляется через pr. Таким образом, для полного объекта требуется как минимум два vptr, чтобы указать на две разные vtable.

person James Kanze    schedule 31.03.2011
comment
на самом деле в типичной реализации f, вероятно, предшествовало бы g или h в виртуальной таблице, поэтому при полиморфном использовании L или R в качестве B корректировка не требуется (то есть макет таблицы L будет надмножеством макета таблицы). B таблицы, так что ее B просмотр соответствует B попыткам). - person Matthieu M.; 31.03.2011
comment
@ Матье М. Да. Во многих реализациях vtable будет начинаться с одного или нескольких специальных указателей, например, на информацию type_info, за которой следуют виртуальные функции в базовых классах по порядку, а затем виртуальные функции класса, которые не являются переопределениями база. Однако факт остается фактом: vtable для L будет иметь g в том же месте, что и vtable для R, имеет h, поэтому одна и та же vtable не может использоваться как для L, так и для R в D. - person James Kanze; 01.04.2011
comment
точно, во многих реализациях. Создание более разумных виртуальных таблиц требует анализа всей программы/оптимизации времени компоновки. - person Matthieu M.; 01.04.2011
comment
Это интересное/убедительное объяснение. Спасибо! - person Alok Save; 04.04.2011
comment
@MatthieuM. макет таблицы L будет надмножеством инварианты таблицы L будут... - person curiousguy; 20.11.2018