Для устранения неоднозначности данных при ромбовидном наследовании в С++ реализован специальный механизм виртуального наследования. Показанное на рис. 10.1 «ромбовидное» наследование на С++ записывается так (см. п. п. 10.1/5 в [1]):
class А { /*...*/};
class Bl: virtual public A { /*...*/}; class B2: virtual public A { /*...*/}: class D: public Bl. public B2 {/*...*/ };
Классы Bl и В2 виртуально наследуют от базового класса А. Класс А является для них виртуальным базовым классом. Виртуальное наследование приводит к тому, что в классе D оказывается единственная копия полей класса А. Однако неоднозначности полей классов В1 и В 2 это не устраняет, как и неоднозначности их методов.
И как мы уже неоднократно убеждались, за все приходится платить. Множественное наследование превращает древовидную иерархию наследования в ациклический граф наследования. При простом наследовании в дереве наследования от, предка к наследнику всегда существует только один путь независимо от кЬли-чества уровней наследования. При множественном наследовании путей в графе наследования может быть несколько (очевидно, при ромбовидном наследовании их два).
При обычном одиночном наследовании конструктор класса-потомка всегда вызывает конструктор непосредственного базового класса для инициализации полей базового класса. Этот порядок сохраняется на любом уровне иерархии. В обычном (не виртуальном) множественном наследовании работает то же правило. Если же мы рассмотрим с этой точки зрения тривиальную ромбовидную иерархию с виртуальным наследованием, то тут же возникает вопрос: какой из классов (В1 или В2) отвечает за инициализацию единственной копии общего базового класса? Опять неоднозначность!
Эта неоднозначность устраняется следующим правилом: виртуальный базовый класс инициализируется последним производным классом в иерархии. Данное правило поддерживается компилятором. Чтобы убедиться в том, что инициализацию виртуальной базы действительно обязан выполнять последний производный класс, напишем пример с виртуальным наследованием, но вызовы конструкторов пропишем так, как при обычном наследовании: в конструкторе наследника будет вызываться конструктор базового класса (листинг 10.2).
lass Bl: virtual public A int b; public:
Bl(const int &a, const int &b):A(a) { this->b = b; }
lass B2: virtual public A int b; public:
B2(const int &a, const int &b):A(a) { this->b = b; }
lass D: public Bl, public B2 double y; public:
D(int bl, int b2, double y) :Bl(0,bl), B2(0,b2) { this->y = y: }
// конструктор инициализации
// виртуальное наследование
// инициализация предка
// неоднозначность с В1
// инициализация предка
// инициализация предков // ошибка трансляции!
Попытка откомпилировать эту иерархию классов в системе Visual C++.NET 2003 приводит к ошибке трансляции С2512 в конструкторе класса D:
А::А: no appropriate default constructor available
Сообщение говорит об отсутствии подходящего конструктора А по умолчанию. Причина в том, что мы не написали вызов конструктора виртуальной базы А в классе D — самом производном классе. Следовательно, транслятор предполагает, что в D необходимо «молча» вызвать конструктор А по умолчанию. Однако в классе А такого нет — отсюда и ошибка! Таким образом, класс D отвечает за инициализацию класса А. Правильная «конструкция» конструктора D должна быть такой, как показано в листинге 10.3.
Листинг 10.3. Правильный конструктор самого производного класса D(int a, int bl, int Ь2, double у)
:А(а), В1(0,Ы), В2(0,Ь2) // инициализация виртуальной базы
{ this->y = у; }
Если вы любите при программировании слушать музыку, тогда вам необходима http://topgoods.com.ua/index.php?categoryID=616 недорого.
Естественно, и «свое» поле конструктор может инициализировать в списке инициализации. Обратите внимание на то, что в конструкторах В1 и В 2 первый аргумент (значение для поля b виртуальной базы) равен нулю. Этот аргумент в данном случае является фиктивным, поскольку у нас есть явный вызов конструктора виртуальной базы А (а). Транслятор правильно во всем разбирается, и поле а инициализируется значением первого аргумента конструктора D, а не нулями. |