Виртуальное наследование в 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:

--------- <- 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 подобъект через указатель. Когда мы можем создать most_derived макет с одной базой, это может выглядеть так:

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

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

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

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

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

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

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

На практике бит строки представляет собой целое число, которое говорит, где фактическое ios начинается относительно адреса производного класса. То есть 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();
}
Другие вопросы по тегам