Jump to content

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

Схема наследования алмазов — проблема, которую пытается решить виртуальное наследование.

Виртуальное наследование — это метод C++ , который гарантирует, что только одна копия базового класса будет переменных-членов унаследована производными классами-внучками. Без виртуального наследования, если два класса B и C наследовать от класса Aи класс D наследует от обоих B и C, затем D будет содержать две копии Aпеременные- члены : одна через Bи один через C. Они будут доступны независимо, используя разрешение области .

Вместо этого, если классы B и C наследовать практически от класса A, то объекты класса D будет содержать только один набор переменных-членов из класса A.

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

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

Рассмотрим следующую иерархию классов.

Виртуальное наследование UML.svg

struct Animal {
    virtual ~Animal() = default;    // Explicitly show that the default class destructor will be made.
    virtual void Eat() {}
};

struct Mammal: Animal {
    virtual void Breathe() {}
};

struct WingedAnimal: Animal {
    virtual void Flap() {}
};

// A bat is a winged mammal
struct Bat: Mammal, WingedAnimal {};

Как было сказано выше, вызов bat.Eat неоднозначно, потому что есть два Animal (косвенные) базовые классы в Bat, так что любой Bat объект имеет два разных Animal подобъекты базового класса. Итак, попытка напрямую привязать ссылку к Animal подобъект a Bat объект потерпит неудачу, поскольку привязка по своей сути неоднозначна:

Bat bat;
Animal& animal = bat;  // error: which Animal subobject should a Bat cast into, 
                // a Mammal::Animal or a WingedAnimal::Animal?

Чтобы устранить неоднозначность, необходимо явно преобразовать bat к любому подобъекту базового класса:

Bat bat;
Animal& mammal = static_cast<Mammal&>(bat); 
Animal& winged = static_cast<WingedAnimal&>(bat);

Чтобы позвонить Eat, необходимо то же самое устранение неоднозначности или явное уточнение: static_cast<Mammal&>(bat).Eat() или static_cast<WingedAnimal&>(bat).Eat() или альтернативно bat.Mammal::Eat() и bat.WingedAnimal::Eat(). Явное уточнение не только использует более простой и единообразный синтаксис как для указателей, так и для объектов, но также допускает статическую отправку, поэтому, возможно, этот метод будет предпочтительным.

В этом случае двойное наследование Animal вероятно, нежелательно, поскольку мы хотим смоделировать отношение ( Bat это Animal) существует только один раз; что Bat это Mammal и является WingedAnimal, не означает, что это Animal дважды: Animal базовый класс соответствует контракту, который Bat реализует (отношение « является » выше на самом деле означает « реализует требования »), а Bat реализует только Animal договор один раз. Реальный смысл фразы « есть только один раз» заключается в том, что Bat должен иметь только один способ реализации Eat, а не двумя разными способами, в зависимости от того, Mammal вид на Bat ест, или WingedAnimal вид на Bat. (В первом примере кода мы видим, что Eat не переопределяется ни в одном Mammal или WingedAnimal, поэтому двое Animal подобъекты на самом деле будут вести себя одинаково, но это всего лишь вырожденный случай, и это не имеет значения с точки зрения C++.)

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

Мы можем переопределить наши классы следующим образом:

struct Animal {
    virtual ~Animal() = default;
    virtual void Eat() {}
};

// Two classes virtually inheriting Animal:
struct Mammal: virtual Animal {
    virtual void Breathe() {}
};

struct WingedAnimal: virtual Animal {
    virtual void Flap() {}
};

// A bat is still a winged mammal
struct Bat: Mammal, WingedAnimal {};

The Animal часть Bat::WingedAnimal сейчас то же самое Animal экземпляр, используемый Bat::Mammal, то есть, что Bat имеет только один общий, Animal экземпляр в его представлении и поэтому вызов Bat::Eat является однозначным. Кроме того, прямой актерский состав из Bat к Animal также однозначно, теперь, когда существует только один Animal экземпляр, который Bat можно конвертировать в.

Возможность поделиться одним экземпляром Animal родитель между Mammal и WingedAnimal включается путем записи смещения памяти между Mammal или WingedAnimal члены и члены базы Animal внутри производного класса. Однако это смещение в общем случае может быть известно только во время выполнения, поэтому Bat должно стать( vpointer, Mammal, vpointer, WingedAnimal, Bat, Animal). Существует два указателя vtable , по одному на каждую иерархию наследования, которая фактически наследует Animal. В этом примере один для Mammal и один для WingedAnimal. Таким образом, размер объекта увеличился на два указателя, но теперь имеется только один Animal и никакой двусмысленности. Все объекты типа Bat будут использовать одни и те же vpointers, но каждый Bat объект будет содержать свой собственный уникальный Animal объект. Если другой класс наследует от Mammal, такой как Squirrel, то vpointer в Mammal часть Squirrel обычно будет отличаться от vpointer в Mammal часть Bat хотя они могут оказаться одинаковыми, если Squirrel класс должен быть такого же размера, как Bat.

Дополнительный пример нескольких предков

[ редактировать ]

Этот пример иллюстрирует случай, когда базовый класс A имеет переменную-конструктор msg и дополнительный предок E является производным от класса внука D.

  A  
 / \  
B   C  
 \ /  
  D 
  |
  E

Здесь, A должны быть построены в обоих D и E. Далее проверка переменной msg иллюстрирует, как класс A становится прямым базовым классом своего производного класса, в отличие от базового класса любого промежуточного производного класса, классифицированного между A и последний производный класс. Приведенный ниже код можно изучить в интерактивном режиме здесь .

#include <string>
#include <iostream>

class A                     { 
    private: 
        std::string _msg; 
    public:
        A(std::string x): _msg(x) {} 
        void test(){ std::cout<<"hello from A: "<<_msg <<"\n"; }
}; 

// B,C inherit A virtually
class B: virtual public A   { public: B(std::string x):A("b"){}  };
class C: virtual public A   { public: C(std::string x):A("c"){}  };
// Compile error when :A("c") is removed (since A's constructor is not called)
//class C: virtual public A   { public: C(std::string x){}  };
//class C: virtual public A   { public: C(std::string x){ A("c"); }  }; // Same compile error

// Since B, C inherit A virtually, A must be constructed in each child
class D: public         B,C { public: D(std::string x):A("d_a"),B("d_b"),C("d_c"){}  }; 
class E: public         D   { public: E(std::string x):A("e_a"),D("e_d"){}  }; 

// Compile error without constructing A
//class D: public         B,C { public: D(std::string x):B(x),C(x){}  };

// Compile error without constructing A
//class E: public         D   { public: E(std::string x):D(x){}  };


int main(int argc, char ** argv){
    D d("d"); 
    d.test(); // hello from A: d_a

    E e("e"); 
    e.test(); // hello from A: e_a
}

Чисто виртуальные методы

[ редактировать ]

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

#include <string>
#include <iostream>

class A                     { 
    protected: 
        std::string _msg; 
    public:
        A(std::string x): _msg(x) {} 
        void test(){ std::cout<<"hello from A: "<<_msg <<"\n"; } 
        virtual void pure_virtual_test() = 0;
}; 

// since B,C inherit A virtually, the pure virtual method pure_virtual_test doesn't need to be defined
class B: virtual public A   { public: B(std::string x):A("b"){}  }; 
class C: virtual public A   { public: C(std::string x):A("c"){}  }; 

// since B,C inherit A virtually, A must be constructed in each child
// however, since D does not inherit B,C virtually, the pure virtual method in A *must be defined*
class D: public B,C { 
    public: 
        D(std::string x):A("d_a"),B("d_b"),C("d_c"){}
        void pure_virtual_test() override { std::cout<<"pure virtual hello from: "<<_msg <<"\n"; }
}; 

// it is not necessary to redefine the pure virtual method after the parent defines it
class E: public D { 
    public: 
    E(std::string x):A("e_a"),D("e_d"){}  
};


int main(int argc, char ** argv){
    D d("d");
    d.test(); // hello from A: d_a
    d.pure_virtual_test(); // pure virtual hello from: d_a

    E e("e"); 
    e.test(); // hello from A: e_a
    e.pure_virtual_test(); // pure virtual hello from: e_a
}
  1. ^ Миля, Андрей. «Решение проблемы алмаза с помощью виртуального наследования» . Cprogramming.com . Проверено 8 марта 2010 г. Одной из проблем, возникающих из-за множественного наследования, является проблема алмаза. Классическую иллюстрацию этому дает Бьярн Страуструп (создатель C++) в следующем примере:
  2. ^ МакАрделл, Ральф (14 февраля 2004 г.). «C++/Что такое виртуальное наследование?» . Все Эксперты . Архивировано из оригинала 10 января 2010 г. Проверено 8 марта 2010 г. Это может потребоваться, если вы используете множественное наследование. В этом случае класс может быть производным от других классов, имеющих тот же базовый класс. В таких случаях без виртуального наследования ваши объекты будут содержать более одного подобъекта базового типа, общего для базовых классов. Является ли это требуемым эффектом, зависит от обстоятельств. Если это не так, вы можете использовать виртуальное наследование, указав виртуальные базовые классы для тех базовых типов, для которых весь объект должен содержать только один такой подобъект базового класса.
Arc.Ask3.Ru: конец переведенного документа.
Arc.Ask3.Ru
Номер скриншота №: 6dba7a53dcbedd3727312b8e107eedfb__1697079000
URL1:https://arc.ask3.ru/arc/aa/6d/fb/6dba7a53dcbedd3727312b8e107eedfb.html
Заголовок, (Title) документа по адресу, URL1:
Virtual inheritance - Wikipedia
Данный printscreen веб страницы (снимок веб страницы, скриншот веб страницы), визуально-программная копия документа расположенного по адресу URL1 и сохраненная в файл, имеет: квалифицированную, усовершенствованную (подтверждены: метки времени, валидность сертификата), открепленную ЭЦП (приложена к данному файлу), что может быть использовано для подтверждения содержания и факта существования документа в этот момент времени. Права на данный скриншот принадлежат администрации Ask3.ru, использование в качестве доказательства только с письменного разрешения правообладателя скриншота. Администрация Ask3.ru не несет ответственности за информацию размещенную на данном скриншоте. Права на прочие зарегистрированные элементы любого права, изображенные на снимках принадлежат их владельцам. Качество перевода предоставляется как есть. Любые претензии, иски не могут быть предъявлены. Если вы не согласны с любым пунктом перечисленным выше, вы не можете использовать данный сайт и информация размещенную на нем (сайте/странице), немедленно покиньте данный сайт. В случае нарушения любого пункта перечисленного выше, штраф 55! (Пятьдесят пять факториал, Денежную единицу (имеющую самостоятельную стоимость) можете выбрать самостоятельно, выплаичвается товарами в течение 7 дней с момента нарушения.)