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

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

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;
}

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

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 может, конечно, иметь более одного члена данных, но это слишком сложно представить в этом простом псевдокоде).

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

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

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

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

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

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

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