Множественное наследование: размер класса для виртуальных указателей? - PullRequest
6 голосов
/ 16 марта 2012

С учетом кода:

class A{};

class B : public virtual A{};

class C : public virtual A{};

class D : public B,public C{};

int main(){
cout<<"sizeof(D)"<<sizeof(D);
return 0;
}

Выход: sizeof (D) 8

Каждый класс содержит свой собственный виртуальный указатель, не принадлежащий ни одному из его базовых классов, Итак, почему размер класса (D) 8?

Ответы [ 4 ]

3 голосов
/ 16 марта 2012

Это зависит от реализации компилятора. Мой компилятор - Visual Stdio C ++ 2005.

Код такой:

int main(){
    cout<<"sizeof(B):"<<sizeof(B) << endl;
    cout<<"sizeof(C):"<<sizeof(C) << endl;
    cout<<"sizeof(D):"<<sizeof(D) << endl;
    return 0;
} 

Будет выводиться

sizeof(B):4
sizeof(C):4
sizeof(D):8

класс B имеет только один виртуальный указатель. Так что sizeof(B)=4. И класс С. тоже.

Но D множественное наследование class B и class C. Компиляция не объединяет две виртуальные таблицы. Поэтому class D имеет две виртуальные точки указателя на каждую виртуальную таблицу.

Если D наследует только один класс, а не виртуальное наследование. Это объединит их виртуальную таблицу.

2 голосов
/ 16 марта 2012

Это зависит от реализации компилятора, поэтому вы должны указать, какой компилятор вы используете. В любом случае D происходит от двух классов, поэтому содержит указатели на B и C vtables указатели базового класса (I не знаю хорошего имени для этого).

Чтобы проверить это, вы можете объявить указатель на B и указатель на C и привести адрес D к базе указатель класса. Сбросьте эти значения, и вы увидите, что они разные!

EDIT
Тест сделан с Visual C ++ 10.0, 32 бит.

class Base
{
};

class Derived1 : public virtual Base
{
};

class Derived2 : public virtual Base
{
};

class Derived3 : public virtual Base
{
};

class ReallyDerived1 : public Derived1, public Derived2, public Derived3
{
};

class ReallyDerived2 : public Derived1, public Derived2
{
};

class ReallyDerived3 : public Derived2
{
};

void _tmain(int argc, _TCHAR* argv[])
{
 std::cout << "Base: " << sizeof(Base) << std::endl;
 std::cout << "Derived1: " <<  sizeof(Derived1) << std::endl;
 std::cout << "ReallyDerived1: " <<  sizeof(ReallyDerived1) << std::endl;
 std::cout << "ReallyDerived2: " <<  sizeof(ReallyDerived2) << std::endl;
 std::cout << "ReallyDerived3: " <<  sizeof(ReallyDerived3) << std::endl;
}

Вывод, думаю, не удивителен:

  • База: 1 байт (ОК, это сюрприз, по крайней мере, для меня).
  • Производное1: 4 байта
  • ReallyDerived1: 12 байт (4 байта на базовый класс из-за множественного наследования)
  • ReallyDerived2: 8 байт (как и предполагалось)
  • ReallyDerived3: 4 байта (только один базовый класс с виртуальным наследованием в пути, но это не виртуальный).

Добавляя виртуальный метод к базе, вы получаете на 4 байта больше для каждого класса. Поэтому, вероятно, дополнительные байты - это не указатели vtable, а указатели базового класса, используемые в множественном наследовании, это поведение не меняет удаление виртуального наследования (но если оно не виртуальное, размер не изменяется при добавлении большего количества баз).

1 голос
/ 16 марта 2012

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

Часто несколько классов в иерархии могут совместно использовать один и тот же vptr.Чтобы это произошло, необходимо, чтобы их смещение в конечном классе было одинаковым, а список записей vtable в базовом классе должен быть начальной последовательностью - список записей vtable в производном классе.

Оба условия соблюдаются практически во всех реализациях для одиночного наследования.Независимо от того, насколько глубоко наследуется, обычно будет только один vptr, общий для всех классов.

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

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

Если мы возьмем вашу иерархию, добавим виртуальные функции, чтобы мы наверняка имелиvptr, мы замечаем, что B и D все еще могут совместно использовать vtable, но для A и C требуется отдельное vtables.Это означает, что если бы у ваших классов были виртуальные функции, вам понадобилось бы как минимум три vptr.(Из этого я заключаю, что ваша реализация использует отдельные указатели на виртуальную базу. B и D используют один и тот же указатель, а C - собственный указатель. И, конечно же, A не имеетвиртуальная база и не нуждается в указателе на себя.)

Если вы пытаетесь точно проанализировать, что происходит, я бы предложил добавить новую виртуальную функцию в каждый класс и добавитьцелочисленный тип указателя размера, который вы инициализируете с различным известным значением для каждого класса.(Используйте конструкторы для установки значения.) Затем создайте экземпляр класса, возьмите его адрес, затем выведите адрес для каждого базового класса.А затем сбросьте класс: известные фиксированные значения помогут определить, где находятся различные элементы.Что-то вроде:

struct VB
{
    int vb;
    VB() : vb( 1 ) {}
    virtual ~VB() {}
    virtual void fvb() {}
};

struct Left : virtual VB
{
    int left;
    Left() : left( 2 ) {}
    virtual ~Left() {}
    virtual void fvb() {}
    virtual void fleft() {}
};

struct Right : virtual VB
{
    int right;
    Right() : right( 3 ) {}
    virtual ~Right() {}
    virtual void fvb() {}
    virtual void fright() {}
};

struct Derived : Left, Right
{
    int derived;
    Derived() : derived( 5 ) {}
    virtual ~Derived() {}
    virtual void fvb() {}
    virtual void fleft() {}
    virtual void fright() {}
    virtual void fderived() {}
};

Вы можете добавить Derived2, который происходит от Derived и посмотреть, что происходит с относительными адресами между, например, Left и VB в зависимости от того, является ли объектимеет тип Derived или Derived2.

0 голосов
/ 16 марта 2012

Вы делаете слишком много предположений.Это сильно зависит от ABI, поэтому вам следует изучить документацию для вашей платформы (я предполагаю, что вы работаете на 32-битной платформе).

Первое, что в вашей виртуальной функции нетпример, и это означает, что ни один из типов на самом деле не содержит указатель на виртуальную таблицу.Так откуда взялись эти 2 указателя?(Я предполагаю, что вы на 32-битной архитектуре).Ну, виртуальное наследство - это ответ.Когда вы наследуете виртуально, относительное расположение виртуальной базы (A) по отношению к дополнительным элементам в производном типе (B, C) будет изменяться по всей цепочке наследования.В случае объекта B или C компилятор может указывать типы как [A, B '] и [A, C'] (где X '- дополнительные поля X, отсутствующие в A).

Теперь виртуальное наследование означает, что в случае D будет только один подобъект A, поэтому компилятор может расположить тип D как [A, B ', C', D] или [A, C ', B ', D] (или любая другая комбинация, A может находиться в конце объекта и т. Д., Это определено в ABI).Итак, что это означает, это означает, что функции-члены B и C не могут предполагать, где может находиться подобъект A (в случае невиртуального наследования, относительное местоположение известно), потому что тип complete на самом деле может быть какой-то другой тип вниз по цепочке.

Решение проблемы состоит в том, что и B, и C обычно содержат дополнительный указатель на базу указатель, аналогичный, но не эквивалентный виртуальному указателю.Так же, как vptr используется для динамической отправки функции, этот дополнительный указатель используется для динамического поиска базы.

Если вас интересуют все эти детали, я рекомендую вам прочитать Itanium ABI , который широко используется не только в Itanium, но и в других архитектурах Intel 64 (и модифицированной версии в 32 архитектурах) различными компиляторами.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...