<< Пред. стр. 91 (из 121) След. >>
foobar( float cval ){
int dval;
// вопросы упражнения относятся к коду, начинающемуся с этого места ...
}
Присвойте локальной переменной dval сумму значений члена dval класса Base1 и члена dval класса Derived.
Присвойте вещественную часть члена cval класса MI члену fval класса Base2.
Присвойте значение члена cval класса Base1 первому символу члена sval класса Derived.
Упражнение 18.12
Дана следующая иерархия классов, в которых имеются функции-члены print():
class Base {
public:
void print( string ) const;
// ...
};
class Derived1 : public Base {
public:
void print( int ) const;
// ...
};
class Derived2 : public Base {
public:
void print( double ) const;
// ...
};
class MI : public Derived1, public Derived2 {
public:
void print( complex
// ...
};
Почему приведенный фрагмент дает ошибку компиляции?
MI mi;
string dancer( "Nejinsky" );
mi.print( dancer );
Как изменить определение MI, чтобы этот фрагмент компилировался и выполнялся правильно?
18.5. Виртуальное наследование A
По умолчанию наследование в C++ является специальной формой композиции по значению. Когда мы пишем:
class Bear : public ZooAnimal { ... };
каждый объект Bear содержит все нестатические данные-члены подобъекта своего базового класса ZooAnimal, а также нестатические члены, объявленные в самом Bear. Аналогично, если производный класс является базовым для какого-то другого:
class PolarBear : public Bear { ... };
то каждый объект PolarBear содержит все нестатические члены, объявленные в PolarBear, Bear и ZooAnimal.
В случае одиночного наследования эта форма композиции по значению, поддерживаемая механизмом наследования, обеспечивает компактное и эффективное представление объекта. Проблемы возникают только при множественном наследовании, когда некоторый базовый класс неоднократно встречается в иерархии наследования. Самый известный реальный пример такого рода – это иерархия классов iostream. Взгляните еще раз на рис. 18.2: istream и ostream наследуют одному и тому абстрактному базовому классу ios, а iostream является производным как от istream, так и от ostream.
class iostream :
public istream, public ostream { ... };
По умолчанию каждый объект iostream содержит два подобъекта ios: из istream и из ostream. Почему это плохо? С точки зрения эффективности хранение двух копий подобъекта ios – пустая трата памяти, поскольку объекту iostream нужен только один экземпляр. Кроме того, конструктор вызывается для каждого подобъекта. Более серьезной проблемой является неоднозначность, к которой приводит наличие двух экземпляров. Например, любое неквалифицированное обращение к члену класса ios дает ошибку компиляции. Какой экземпляр имеется в виду? Что будет, если классы istream и ostream инициализируют свои подобъекты ios по-разному? Можно ли гарантировать, что в классе iostream используется согласованная пара членов ios? Применяемый по умолчанию механизм композиции по значению не дает таких гарантий.
Для решения данной проблемы язык предоставляет альтернативный механизм композиции по ссылке: виртуальное наследование. В этом случае наследуется только один разделяемый подобъект базового класса, независимо от того, сколько раз базовый класс встречается в иерархии наследования. Этот разделяемый подобъект называется виртуальным базовым классом. С помощью виртуального наследования снимаются проблемы дублирования подобъектов базового класса и неоднозначностей, к которым такое дублирование приводит.
Для изучения синтаксиса и семантики виртуального наследования мы выбрали класс Panda. В зоологических кругах уже на протяжении ста лет периодически вспыхивают ожесточенные споры по поводу того, к какому семейству относить панду: к медведям или к енотам. Поскольку проектирование программного обеспечения призвано обслуживать, в основном, интересы прикладных областей, то самое правильное – произвести класс Panda от обоих классов:
class Panda : public Bear,
public Raccoon, public Endangered { ... };
Наша виртуальная иерархия наследования Panda показана на рис. 18.4: две пунктирные стрелки обозначают виртуальное наследование классов Bear и Raccoon от ZooAnimal, а три сплошные – невиртуальное наследование Panda от Bear, Raccoon и, на всякий случай, от класса Endangered из раздела 18.2.
ZooAnimal Endangered
Bear Raccoon
Panda
??> невиртуальное наследование
- - - -> виртуальное наследование
Рис. 18.4. Иерархия виртуального наследования класса Panda
На данном рисунке показан интуитивно неочевидный аспект виртуального наследования: оно (в нашем случае наследование классов Bear и Raccoon) должно появиться в иерархии раньше, чем в нем возникнет реальная необходимость. Необходимым виртуальное наследование становится только при объявлении класса Panda, но если перед этим базовые классы Bear и Raccoon не наследуют своему базовому виртуально, то проектировщику класса Panda не повезло.
Должны ли мы производить свои базовые классы виртуально просто потому, что где-то ниже в иерархии может потребоваться виртуальное наследование? Нет, это не рекомендуется: снижение производительности и усложнение дальнейшего наследования может оказаться существенным (см. [LIPPMAN96a], где приведены и обсуждаются результаты измерения производительности).
Когда же использовать виртуальное наследование? Чтобы его применение было успешным, иерархия, например библиотека iostream или наше дерево классов Panda, должна проектироваться целиком либо одним человеком, либо коллективом разработчиков.
В общем случае мы не рекомендуем пользоваться виртуальным наследованием, если только оно не решает конкретную проблему проектирования. Однако посмотрим, как все-таки можно его применить.
18.5.1. Объявление виртуального базового класса
Для указания виртуального наследования в объявление базового класса вставляется модификатор virtual. Так, в данном примере ZooAnimal становится виртуальным базовым для Bear и Raccoon:
// взаимное расположение ключевых слов public и virtual
// несущественно
class Bear : public virtual ZooAnimal { ... };
class Raccoon : virtual public ZooAnimal { ... };
Виртуальное наследование не является явной характеристикой самого базового класса, а лишь описывает его отношение к производному. Как мы уже отмечали, виртуальное наследование – это разновидность композиции по ссылке. Иначе говоря, доступ к подобъекту и его нестатическим членам косвенный, что обеспечивает гибкость, необходимую для объединения нескольких виртуально унаследованных подобъектов базовых классов в один разделяемый экземпляр внутри производного. В то же время объектом производного класса можно манипулировать через указатель или ссылку на тип базового, хотя последний является виртуальным. Например, все показанные ниже преобразования базовых классов Panda выполняются корректно, хотя Panda использует виртуальное наследование:
extern void dance( const Bear* );
extern void rummage( const Raccoon* );
extern ostream&
operator<<( ostream&, const ZooAnimal& );
int main()
{
Panda yin_yang;
dance( &yin_yang ); // правильно
rummage( &yin_yang ); // правильно
cout << yin_yang; // правильно
// ...
}
Любой класс, который можно задать в качестве базового, разрешается сделать виртуальным, причем он способен содержать все те же элементы, что обычные базовые классы. Так выглядит объявление ZooAnimal:
#include
#include
class ZooAnimal;
extern ostream&
operator<<( ostream&, const ZooAnimal& );
class ZooAnimal {
public:
ZooAnimal( string name,
bool onExhibit, string fam_name )
: _name( name ),
_onExhibit( onExhibit ), _fam_name( fam_name )
{}
virtual ~ZooAnimal();
virtual ostream& print( ostream& ) const;
string name() const { return _name; }
string family_name() const { return _fam_name; }
// ...
protected:
bool _onExhibit;
string _name;
string _fam_name;
// ...
};
К объявлению и реализации непосредственного базового класса при использовании виртуального наследования добавляется ключевое слово virtual. Вот, например, объявление нашего класса Bear:
class Bear : public virtual ZooAnimal {
public:
enum DanceType {
two_left_feet, macarena, fandango, waltz };
Bear( string name, bool onExhibit=true )
: ZooAnimal( name, onExhibit, "Bear" ),
_dance( two_left_feet )
{}
virtual ostream& print( ostream& ) const;
void dance( DanceType );
// ...
protected:
DanceType _dance;
// ...
};
А вот объявление класса Raccoon:
class Raccoon : public virtual ZooAnimal {
public:
Raccoon( string name, bool onExhibit=true )
: ZooAnimal( name, onExhibit, "Raccoon" ),
_pettable( false )
{}
virtual ostream& print( ostream& ) const;
bool pettable() const { return _pettable; }
void pettable( bool petval ) { _pettable = petval; }
// ...
protected:
bool _pettable;
// ...
};
18.5.2. Специальная семантика инициализации
Наследование, в котором присутствует один или несколько виртуальных базовых классов, требует специальной семантики инициализации. Взгляните еще раз на реализации Bear и Raccoon в предыдущем разделе. Видите ли вы, какая проблема связана с порождением класса Panda?
class Panda : public Bear,
public Raccoon, public Endangered {
public:
Panda( string name, bool onExhibit=true );
virtual ostream& print( ostream& ) const;
bool sleeping() const { return _sleeping; }
void sleeping( bool newval ) { _sleeping = newval; }
// ...
protected:
bool _sleeping;
// ...
};
Проблема в том, что конструкторы базовых классов Bear и Raccoon вызывают конструктор ZooAnimal с неявным набором аргументов. Хуже того, в нашем примере значения по умолчанию для аргумента fam_name (название семейства) не только отличаются, они еще и неверны для Panda.
В случае невиртуального наследования производный класс способен явно инициализировать только свои непосредственные базовые классы (см. раздел 17.4). Так, классу Panda, наследующему от ZooAnimal, не разрешается напрямую вызвать конструктор ZooAnimal в своем списке инициализации членов. Однако при виртуальном наследовании только Panda может напрямую вызывать конструктор своего виртуального базового класса ZooAnimal.
Ответственность за инициализацию виртуального базового возлагается на ближайший производный класс. Например, когда объявляется объект класса Bear:
Bear winnie( "pooh" );
то Bear является ближайшим производным классом для объекта winnie, поэтому выполняется вызов конструктора ZooAnimal, определенный в классе Bear. Когда мы пишем:
cout << winnie.family_name();
будет выведена строка:
The family name for pooh is Bear
(Название семейства для pooh – это Bear)
Аналогично для объявления
Raccoon meeko( "meeko" );
Raccoon – это ближайший производный класс для объекта meeko, поэтому выполняется вызов конструктора ZooAnimal, определенный в классе Raccoon. Когда мы пишем:
cout << meeko.family_name();
печатается строка:
The family name for meeko is Raccoon
(Название семейства для meeko - это Raccoon)
Если же объявить объект типа Panda:
Panda yolo( "yolo" );
то ближайшим производным классом для объекта yolo будет Panda, поэтому он и отвечает за инициализацию ZooAnimal.
Когда инициализируется объект Panda, то явные вызовы конструктора ZooAnimal в конструкторах классов Raccoon и Bear не выполняются, а вызывается он с теми аргументами, которые указаны в списке инициализации членов объекта Panda. Вот так выглядит реализация:
Panda::Panda( string name, bool onExhibit=true )
: ZooAnimal( name, onExhibit, "Panda" ),
Bear( name, onExhibit ),
Raccoon( name, onExhibit ),
Endangered( Endangered::environment,
Endangered::critical ),
sleeping( false )
{}
Если в конструкторе Panda аргументы для конструктора ZooAnimal не указаны явно, то вызывается конструктор ZooAnimal по умолчанию либо, если такового нет, выдается ошибка при компиляции определения конструктора Panda.
Когда мы пишем:
cout << yolo.family_name();
печатается строка:
The family name for yolo is Panda
(Название семейства для yolo - это Panda)
Внутри определения Panda классы Raccoon и Bear являются промежуточными, а не ближайшими производными. В промежуточном производном классе все прямые вызовы конструкторов виртуальных базовых классов автоматически подавляются. Если бы от Panda был в дальнейшем произведен еще один класс, то сам класс Panda стал бы промежуточным и вызов из него конструктора ZooAnimal также был бы подавлен.
Обратите внимание, что оба аргумента, передаваемые конструкторам Bear и Raccoon, излишни в том случае, когда они выступают в роли промежуточных производных классов. Чтобы избежать передачи ненужных аргументов, мы можем предоставить явный конструктор, вызываемый, когда класс оказывается промежуточным производным. Изменим наш конструктор Bear:
class Bear : public virtual ZooAnimal {
public:
// если выступает в роли ближайшего производного класса
Bear( string name, bool onExhibit=true )
: ZooAnimal( name, onExhibit, "Bear" ),
_dance( two_left_feet )
{}
// ... остальное без изменения
protected:
// если выступает в роли промежуточного производного класса
Bear() : _dance( two_left_feet ) {}
// ... остальное без изменения
};
Мы сделали этот конструктор защищенным, поскольку он вызывается только из производных классов. Если аналогичный конструктор по умолчанию обеспечен и для класса Raccoon, можно следующим образом модифицировать конструктор Panda:
Panda::Panda( string name, bool onExhibit=true )
: ZooAnimal( name, onExhibit, "Panda" ),
Endangered( Endangered::environment,
Endangered::critical ),
sleeping( false )
{}
18.5.3. Порядок вызова конструкторов и деструкторов
Виртуальные базовые классы всегда конструируются перед невиртуальными, вне зависимости от их расположения в иерархии наследования. Например, в приведенной иерархии у класса TeddyBear (плюшевый мишка) есть два виртуальных базовых: непосредственный – ToyAnimal (игрушечное животное) и экземпляр ZooAnimal, от которого унаследован класс Bear:
class Character { ... }; // персонаж
class BookCharacter : public Character { ... };
// литературный персонаж
class ToyAnimal { ... }; // игрушка
class TeddyBear : public BookCharacter,
public Bear, public virtual ToyAnimal
{ ... };
Эта иерархия изображена на рис. 18.5, где виртуальное наследование показано пунктирной стрелкой, а невиртуальное – сплошной.
Character ZooAnimal ToyAnimal
BookCharacter Bear
TeddyBear
??> невиртуальное наследование
- - - -> виртуальноe наследование
Рис. 18.5. Иерархия виртуального наследования класса TeddyBear
Непосредственные базовые классы просматриваются в порядке их объявления при поиске среди них виртуальных. В нашем примере сначала анализируется поддерево наследования BookCharacter, затем Bear и наконец ToyAnimal. Каждое поддерево обходится в глубину, т.е. поиск начинается с корневого класса и продвигается вниз. Так, для поддерева BookCharacter сначала просматривается Character, а затем BookCharacter. Для поддерева Bear – ZooAnimal, а потом Bear.
При описанном алгоритме поиска порядок вызова конструкторов виртуальных базовых классов для TeddyBear таков: ZooAnimal, потом ToyAnimal.
После того как вызваны конструкторы виртуальных базовых классов , настает черед конструкторов невиртуальных, которые вызываются в порядке объявления: BookCharacter, затем Bear. Перед выполнением конструктора BookCharacter вызывается конструктор его базового класса Character.
Если имеется объявление:
TeddyBear Paddington;
то последовательность вызова конструкторов базовых классов будет такой:
ZooAnimal(); // виртуальный базовый класс Bear
ToyAnimal(); // непосредственный виртуальный базовый класс
Character(); // невиртуальный базовый класс BookCharacter
BookCharacter(); // непосредственный невиртуальный базовый класс
Bear(); // непосредственный невиртуальный базовый класс
TeddyBear(); // ближайший производный класс
причем за инициализацию ZooAnimal и ToyAnimal отвечает TeddyBear – ближайший производный класс объекта Paddington.
Порядок вызова копирующих конструкторов при почленной инициализации (и копирующих операторов присваивания при почленном присваивании) такой же. Гарантируется, что деструкторы вызываются в последовательности, обратной вызову конструкторов.
18.5.4. Видимость членов виртуального базового класса
Изменим наш класс Bear так, чтобы он имел собственную реализацию функции-члена onExhibit(), предоставляемой также ZooAnimal:
bool Bear::onExhibit() { ... }
Теперь обращение к onExhibit() через объект Bear разрешается в пользу экземпляра, определенного в этом классе:
Bear winnie( "любитель меда" );
winnie.onExhibit(); // Bear::onExhibit()
Обращение же к onExhibit() через объект Raccoon разрешается в пользу функции-члена, унаследованной из ZooAnimal:
Raccoon meeko( "любитель всякой еды" );
meeko.onExhibit(); // ZooAnimal::onExhibit()
Производный класс Panda наследует члены своих базовых классов. Их можно отнести к одной из трех категорий:
члены виртуального базового класса ZooAnimal, такие, как name() и family(), не замещенные ни в Bear, ни в Raccoon;
член onExhibit() виртуального базового класса ZooAnimal, наследуемый при обращении через Raccoon и замещенный в классе Bear;
специализированные в классах Bear и Raccoon экземпляры функции print() из ZooAnimal.
Можно ли, не опасаясь неоднозначности, напрямую обращаться к унаследованным членам из области видимости класса Panda? В случае невиртуального наследования – нет: все неквалифицированные ссылки на имя неоднозначны. Что касается виртуального наследования, то прямое обращение допустимо к любым членам из первой и второй категорий. Например, дан объект класса Panda:
Panda spot( "Spottie" );
Тогда инструкция
spot.name();
вызывает разделяемую функцию-член name() виртуального базового ZooAnimal, а инструкция
spot.onExhibit();
вызывает функцию-член onExhibit() производного класса Bear.
Когда два или более экземпляров члена наследуются разными путями (это относится не только к функциям-членам, но и к данным-членам, а также к вложенным типам) и все они представляют один и тот же член виртуального базового класса, неоднозначности не возникает, поскольку существует единственный разделяемый экземпляр (первая категория). Если один экземпляр представляет член виртуального базового, а другой – член унаследованного от него класса, то неоднозначности также не возникает: специализированному экземпляру из производного класса отдается предпочтение по сравнению с разделяемым экземпляром из виртуального базового (вторая категория). Но если оба экземпляра представляют члены производных классов, то прямое обращение неоднозначно. Лучше всего разрешить эту ситуацию, предоставив замещающий экземпляр в производном классе (третья категория).
Например, при невиртуальном наследовании неквалифицированное обращение к onExhibit() через объект Panda неоднозначно:
// ошибка: неоднозначно при невиртуальном наследовании
Panda yolo( "любитель бамбука" );
yolo.onExhibit();
В данном случае все унаследованные экземпляры имеют равные приоритеты при разрешении имени, поэтому неквалифицированное обращение приводит к ошибке компиляции из-за неоднозначности (см. раздел 18.4.1).
При виртуальном наследовании члену, унаследованному из виртуального базового класса, приписывается меньший приоритет, чем члену с тем же именем, замещенному в производном. Так, унаследованному от Bear экземпляру onExhibit() отдается предпочтение перед экземпляром из ZooAnimal, унаследованному через Raccoon: