- Виртуальное наследование
-
- Про наследование виртуальных методов, см виртуальный метод.
Виртуа́льное насле́дование (англ. virtual inheritance) в языке программирования C++ — один из вариантов наследования, который нужен для решения некоторых проблем, порождаемых наличием возможности множественного наследования (особенно «ромбовидного наследования»), путем разрешения неоднозначности того, методы которого из суперклассов (непосредственных классов-предков) необходимо использовать. Оно применяется в тех случаях, когда множественное наследование вместо предполагаемой полной композиции свойств классов-предков приводит к ограничению доступных наследуемых свойств вследствие неоднозначности. Базовый класс, наследуемый множественно, определяется виртуальным с помощью ключевого слова
virtual
.Содержание
Суть проблемы
Рассмотрим следующую иерархию классов:
class Animal { public: virtual void eat(); // Метод определяется для данного класса }; class Mammal : public Animal { public: virtual Color getHairColor(); }; class WingedAnimal : public Animal { public: virtual void flap(); }; // A bat is a winged mammal class Bat : public Mammal, public WingedAnimal {}; //<--- обратите внимание, что метод eat() не переопределен в Bat Bat bat;
Но как летучая мышь ест? Что происходит при вызове метода
bat.eat()
?
В вышеприведенном коде вызовbat.eat()
является неоднозначным. Он может относиться как кBat::WingedAnimal::Animal::eat()
так и кBat::Mammal::Animal::eat()
. И у каждого наследника (WingedAnimal, Mammal) метод eat() определен по-своему. Проблема в том, что семантика традиционного множественного наследования не соответствует моделируемой им реальности. В некотором смысле, сущностьAnimal
единственна по сути;Bat
— этоMammal
иWingedAnimal
, но свойство животности (Animal
ness) летучей мыши (Bat
), оно же свойство животности млекопитающего (Mammal
) и оно же свойство животностиWingedAnimal
— по сути это одно и то же свойство.Такая ситуация обычно именуется diamond inheritance («ромбическое наследование») и представляет из себя проблему, которую призвано решить виртуальное наследование.
Представление класса
Прежде чем продолжить, полезным будет рассмотреть, как классы представляются в C++. В частности, при наследовании классы предка и наследника просто помещаются в памяти друг за другом. Таким образом объект класса Bat это на самом деле последовательность объектов классов (Animal,Mammal,Animal,WingedAnimal,Bat), размещенных последовательно в памяти, при этом Animal повторяется дважды, что и приводит к неоднозначности.
Решение
Мы можем переопределить наши классы следующим образом:
class Animal { public: virtual void eat(); }; // Two classes virtually inheriting Animal: class Mammal : public virtual Animal // <--- обратите внимание на ключевое слово virtual { public: virtual Color getHairColor(); }; class WingedAnimal : public virtual Animal // <--- обратите внимание на ключевое слово virtual { public: virtual void flap(); }; // A bat is still a winged mammal class Bat : public Mammal, public WingedAnimal {};
Теперь, часть
Animal
объекта классаBat::WingedAnimal
та же самая, что и частьAnimal
, которая используется вBat::Mammal
, и можно сказать, чтоBat
имеет в своем представлении только одну частьAnimal
и вызовBat::eat()
становится однозначным.Виртуальное наследование реализуется через добавление указателей на виртуальную таблицу vtable в классы
Mammal
иWingedAnimal
, это делается в частности потому, что смещение памяти между началомMammal
и егоAnimal
части неизвестно на этапе компиляции, а выясняется только во время выполнения. Таким образом,Bat
представляется, как (vtable*, Mammal, vtable*, WingedAnimal, Bat, Animal). Два указателя vtable на объект увеличивают размер объекта на величину двух указателей, но это обеспечивает единственностьAnimal
и отсутствие многозначности. Нужны два указателя vtables: по одному на каждого предка в иерархии, который виртуально наследуется отAnimal
: один дляMammal
и один дляWingedAnimal
. Все объекты классаBat
будут иметь одни и те же указатели vtable *, но каждый отдельный объектBat
будет содержать собственную реализацию объектаAnimal
. Если какой-нибудь другой класс будет наследоваться отMammal
, например Squirrel (белка), то vtable* в объектеMammal
объектаSquirrel
будет отличаться от vtable* в объектеMammal
объектаBat
, хотя в особом случае они по-прежнему могут быть одинаковы по сути: когда частьSquirrel
объекта имеет тот же самый размер, что и частьBat
, поскольку, тогда расстояние от реализацииMammal
до частиAnimal
будет одинаковым. Но сами виртуальные таблицы vtables будут все же разными, в отличие от располагаемой в них информации о смещениях.Пример
Чтобы понять суть виртуального наследование без лишнего "шума", следует рассмотреть следующий пример:
#include <iostream> class A { public: int foo() { return 1; } }; class B: public virtual A { public: }; class C : public virtual A { public: }; class D : public B, public C {}; int main() { D d; std::cout << d.foo(); return 0; }
Если убрать ключевое слово virtual, то метод foo() не будет доступен как объект класса D и код не скомпилируется.
См. также
Литература
- Подбельский В. В. Глава 10.2 Множественное наследование и виртуальные базовые классы // Язык Си++ / рец. Дадаев Ю. Г.. — 4. — М.: Финансы и статистика, 2003. — С. 336-359. — 560 с. — ISBN 5-279-02204-7, УДК 004.438Си(075.8) ББК 32.973.26-018 1я173
Категории:- C++
- Наследование (программирование)
Wikimedia Foundation. 2010.