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

Виртуальное наследование — это метод 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
ключевое слово .
Рассмотрим следующую иерархию классов.
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
}
Ссылки
[ редактировать ]- ^ Миля, Андрей. «Решение проблемы алмаза с помощью виртуального наследования» . Cprogramming.com . Проверено 8 марта 2010 г.
Одной из проблем, возникающих из-за множественного наследования, является проблема алмаза. Классическую иллюстрацию этому дает Бьярн Страуструп (создатель C++) в следующем примере:
- ^ МакАрделл, Ральф (14 февраля 2004 г.). «C++/Что такое виртуальное наследование?» . Все Эксперты . Архивировано из оригинала 10 января 2010 г. Проверено 8 марта 2010 г.
Это может потребоваться, если вы используете множественное наследование. В этом случае класс может быть производным от других классов, имеющих тот же базовый класс. В таких случаях без виртуального наследования ваши объекты будут содержать более одного подобъекта базового типа, общего для базовых классов. Является ли это требуемым эффектом, зависит от обстоятельств. Если это не так, вы можете использовать виртуальное наследование, указав виртуальные базовые классы для тех базовых типов, для которых весь объект должен содержать только один такой подобъект базового класса.