Виртуальное наследование в C++

Я нашел это на веб-сайте, читая о виртуальном наследовании в C++

когда используется множественное наследование, иногда необходимо использовать виртуальное наследование. Хорошим примером для этого является стандартная иерархия классов iostream:

//Note: this is a simplified description of iostream classes

class  ostream: virtual public ios { /*..*/ }
class  istream: virtual public ios { /*..*/ }

class iostream : public istream, public ostream { /*..*/ } 
//a single ios inherited

Как C++ гарантирует, что существует только один экземпляр виртуального члена, независимо от количества производных от него классов? C++ использует дополнительный уровень косвенного доступа к виртуальному классу, обычно с помощью указателя. Другими словами, каждый объект в иерархии iostream имеет указатель на общий экземпляр объекта ios. Дополнительный уровень косвенности имеет небольшие накладные расходы на производительность,но это небольшая цена.

я путаюсь с утверждением:

C++ использует дополнительный уровень косвенного доступа к виртуальному классу, обычно с помощью указателя

может ли кто-нибудь объяснить это?

4 ответов


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

class Base {
public:
    int base_member;
};

class Derived: public Base {
public:
    int derived_member;
};


Derived *d = new Derived();
int foo = d->derived_member;  // Only one indirection necessary.
int bar = d->base_member;     // Same here.
delete d;

однако, когда виртуальное наследование вступает в игру, виртуальные базовые члены совместно используются всеми классами в их дереве наследования, вместо нескольких копий, создаваемых при наследовании базового класса. В вашем примере, iostream только содержит одну общую копию ios членов, даже если он наследует их два раза как istream и ostream.

class Base {
public:
    // Shared by Derived from Intermediate1 and Intermediate2.
    int base_member;  
};

class Intermediate1 : virtual public Base {
};

class Intermediate2 : virtual public Base {
};

class Derived: public Intermediate1, public Intermediate2 {
public:
    int derived_member;
};

это означает, что для доступа к виртуальным базовым членам требуется дополнительный косвенный шаг:

Derived *d = new Derived();
int foo = d->derived_member;  // Only one indirection necessary.
int bar = d->base_member;     // Roughly equivalent to
                              // d->shared_Base->base_member.
delete d;

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

struct base { int x; };
struct derived : base { int y };

макет для производных:

--------- <- base & derived start here
    x
---------
    y
---------

если вы добавляете второй производный и самый производный типы (опять же, без виртуального наследования), вы получаете что-то вроде:

struct derived2 : base { int z; };
struct most_derived : derived, derived 2 {};

С этого макета:

--------- <- derived::base, derived and most_derived start here
    x
---------
    y
--------- <- derived2::base & derived2 start here
    x
---------
    z
---------

если у вас most_derived объект и вы связываете указатель / ссылку типа derived2 он будет указывать на линию, отмеченную derived2::base. Теперь, если наследование от base было виртуальным, то должен быть один экземпляр base. Для обсуждения просто предположим, что мы наивно удаляем второе base:

--------- <- derived::base, derived and most_derived start here
    x
---------
    y
--------- <- derived2 start here??
    z
---------

теперь проблема в том, что если мы получаем указатель на derived он имеет тот же макет, что и оригинал, но если мы попытаемся получить указатель на derived2 макет будет отличаться и код derived2 не удалось бы найти x член. Нам нужно сделать что-то умнее, и именно здесь в игру вступает указатель. Путем добавления указателя на каждый объект, который наследуется практически, получаем такую раскладку:

---------  <- derived starts here
base::ptr  --\
    y        |  pointer to where the base object resides
---------  <-/
    x
---------

аналогично derived2. Теперь, за счет дополнительной косвенности, мы можем найти x subobject через указатель. Когда мы можем создать most_derived макет с одной базой, он может выглядеть так:

---------          <- derived starts here
base::ptr  -----\
    y           |
---------       |  <- derived2
base::ptr  --\  |
    z         | |
---------  <--+-/  <- base
    x
---------

теперь код в derived и derived2 nows как получить доступ к базовому подобъекту (просто разыменование base::ptr объект-членов), и в то же время у вас есть один экземпляр base. Если код в любом промежуточном класс доступа x они могут сделать это this->[hidden base pointer]->x, и это будет разрешено во время выполнения в правильном положении.

важный бит здесь - это код, скомпилированный в derived/derived2 layer может использоваться с объектом этого типа или любым производным объектом. Если бы мы написали второй most_derived2 объект, где порядок наследования был отменен, то они макет y и z можно поменять местами, а смещения от указателя на derived или derived2 подобъекты для the base subobject будет отличаться, но код для доступа x все равно будет то же самое: разыменование вашего собственного скрытого базового указателя, гарантируя, что если метод в derived является окончательной overrider, и что доступ к base::x тогда он найдет его независимо от окончательного макета.


в C++ класс выкладывается в память в фиксированном порядке. Базовый класс существует буквально внутри памяти, выделенной производному классу, с фиксированным смещением, аналогично меньшей коробке внутри большей коробки.

если у вас нет виртуального наследования вы говорите iostream содержит элемент istream и ostream каждый из которых содержит ios. Поэтому iostream содержит два ioses.

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

затем iostream содержит элемент istream и ostream каждый из которых связан с ios строкой. Поэтому iostream один ios, связанные двумя отдельными битами строки.

на практике бит строки является целым числом, которое говорит, где фактическое ios запускается относительно адреса производного класса. Т. е. the istream имеет скрытый член называется, например,__virtual_base_offset_ios. Когда istream методы хотят получить доступ к ios база, они берут свои this указатель, добавьте __ios_base_offset и вот это ios указатель базового класса.

-

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


для удаления неоднозначности используется виртуальное наследование.

class base {
    public:
        int a;
};

class new1 :virtual public base
{
    public:
        int b;
};
class new2 :virtual public base
{
    public:
        int c;
};

class drive : public new1,public new2
{
    public:
        void getvalue()
        {
            cout<<"input a b c "<<endl;
            cin>>a>>b>>c;
        }
        void printf()
        {

            cout<<a<<b<<c;
        }
};

int main()
{
    drive ob;
    ob.getvalue();
    ob.printf();
}