Что происходит с виртуальным базовым классом при многоуровневом наследовании?

Играя с наследованием, мне довелось попробовать следующее:

class A
{ int i; };

class B : virtual public A
{ int j; };

class C : public B
{ int k; };

int main()
{
    std::cout<<sizeof(C)/sizeof(int);
    return 0;
}

Что дало мне результат 6

В то время как следующее работало, как ожидалось, давая результат 3

class A
{ int i; };

class B : public A  // No virtual here
{ int j; };

class C : public B
{ int k; };

int main()
{
    std::cout<<sizeof(C)/sizeof(int);
    return 0;
}

Почему такая разница? и почему это вдвое больше, чем во втором случае?


person asheeshr    schedule 08.12.2012    source источник


Ответы (4)


class A {
    int i;
};

class B : public A {
    int j;
};

В этом примере, который не использует виртуальное наследование, объект типа B можно расположить так, как если бы B был определен следующим образом:

class B0 {
    int i;
    int j;
};

Как только вы введете виртуальное наследование, это не сработает:

class C : public virtual A {
    int k;
};

class D : public virtual A {
    int l;
};

class E : public C, public D {
    int m;
};

Объект типа C имеет два int члена: k из определения C и i из определения A. Точно так же объект типа D имеет два int члена, l и i. Все идет нормально. Сложная часть связана с классом E: у него тоже есть один int член i, потому что оба экземпляра A являются виртуальными базами. Таким образом, ни C, ни D не могут быть записаны как B0 выше, потому что тогда E закончится двумя копиями i.

Решение состоит в том, чтобы добавить уровень косвенности. Объекты типа C, D и E выглядят примерно так (псевдокод, не пытайтесь его компилировать):

class C0 {
    int *cip = &i;
    int k;
    int i;
};

class D0 {
    int *dip = &i;
    int l;
    int i;
};

class E0 {
// C0 subobect:
    int *cip = &i;
    int k;
// D0 subobject:
    int *dip = &i;
    int l;
// E data:
    int *eip = &i;
    int m;
    int i;
};

То, что вы видите в размере E, - это те дополнительные указатели, которые позволяют иметь единственную копию i независимо от того, как C и D объединены в производном классе. (На самом деле каждый из этих указателей был бы указателем на A, поскольку A, безусловно, может иметь более одного члена данных, но это слишком сложно представить в этом простом псевдокоде).

person Pete Becker    schedule 08.12.2012
comment
int *cip = &i; под этим вы подразумеваете указатель на унаследованный член i в C? ЕСЛИ так, тогда при виртуальном наследовании наследуются практически только указатели, а не члены с одинаковыми именами? - person asheeshr; 09.12.2012
comment
@AshRj - этот ответ касается деталей реализации. Независимо от того, как реализовано виртуальное наследование, члены виртуальной базы наследуются; вы можете ссылаться на них напрямую через объект производного типа, не зная, что они происходят из базового класса. - person Pete Becker; 09.12.2012

Это зависит от реализации.

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

Строго говоря, вы должны полагаться на размер как на что-то конкретное, и именно по этой причине стандарт предоставляет sizeof, чтобы получить фактический размер, а не предполагать его.

person Alok Save    schedule 08.12.2012

Проще говоря, виртуальное наследование связано с дополнительными накладными расходами. Типичная реализация потребует как минимум дополнительного указателя.

См. Вопрос 4 в Виртуальные таблицы и виртуальные указатели на множественное виртуальное наследование и приведение типов и ответы.

person NPE    schedule 08.12.2012

Это зависит от реализации вашего компилятора. У разных компиляторов разные результаты. Но один уверен, результата должно быть больше трех.

person CodeSun    schedule 08.12.2012