Стенли Липпман - Язык программирования C++. Пятое издание
protected:
int ival;
double dval;
char cval;
private:
int *id;
};
struct Base2 {
void print(double) const; // по умолчанию открыты
protected:
double fval;
private:
double dval;
};
struct Derived : public Base1 {
void print(std::string) const; // по умолчанию открыты
protected:
std::string sval;
double dval;
};
struct MI : public Derived, public Base2 {
void print(std::vector<double>); // по умолчанию открыты
protected:
int *ival;
std::vector<double> dvec;
};
18.3.4. Виртуальное наследование
Хотя список наследования класса не может включать тот же базовый класс несколько раз, класс вполне может унаследовать тот же базовый класс многократно. Тот же базовый класс может быть унаследован косвенно, от двух его собственных прямых базовых классов, либо он может унаследовать некий класс и прямо, и косвенно, через другой из его базовых классов.
Например, библиотечные классы ввода-вывода istream и ostream происходят от общего абстрактного базового класса basic_ios. Этот класс содержит буфер потока и управляет флагом состояния потока. Класс iostream, способный и читать, и писать в поток, происходит непосредственно и от класса istream, и от класса ostream. Поскольку оба класса происходят от класса basic_ios, класс iostream наследует этот базовый класс дважды: один раз от класса istream и один раз от класса ostream.
По умолчанию объект производного класса содержит отдельные части, соответствующие каждому классу в его цепи наследования. Если тот же базовый класс наследуется несколько раз, то у объекта производного класса будет больше одного внутреннего объекта этого типа.
Для такого класса, как iostream, это стандартное поведение не работает. Объект класса iostream должен использовать тот же буфер и для чтения, и для записи, а его флаг должен отражать состояние операций и ввода, и вывода. Если у объекта класса iostream будут две копии объекта класса basic_ios, то их совместное использование невозможно.
В языке С++ для решения этой проблемы используется виртуальное наследование (virtual inheritance). Виртуальное наследование позволяет классу указать, что его базовый класс будет использоваться совместно. Совместно используемый внутренний объект базового класса называется виртуальным базовым классом (virtual base class). Независимо от того, сколько раз тот же базовый виртуальный класс присутствует в иерархии наследования, объект производного класса содержит только один совместно используемый внутренний объект этого виртуального базового класса.
Разные классы PandaВ прошлом велись дебаты о принадлежности вида панда к семейству енотов или медведей. Чтобы отобразить эти сомнения, изменим класс Panda так, чтобы он происходил и от класса Bear, и от класса Raccoon. Чтобы избавить класс Panda от двух частей базового класса ZooAnimal, определим наследование классов Bear и Raccoon от класса ZooAnimal как виртуальное. Новая иерархия представлена на рис. 18.3.
Рис. 18.3. Виртуальное наследование в иерархии класса Panda
Глядя на новую иерархию, можно заметить неочевидный аспект виртуального наследования. Виртуальное наследование должно быть осуществлено прежде, чем в нем возникнет потребность. Например, в этих классах потребность в виртуальном наследовании возникает только при определении класса Panda. Но если бы классы Bear и Raccoon не определили бы свое происхождение от класса ZooAnimal как виртуальное, конструкция класса Panda была бы неудачна.
На практике необходимость наличия промежуточного базового класса при виртуальном наследовании редко создает проблемы. Обычно иерархия классов, в которой используется виртуальное наследование, разрабатывается сразу и одним лицом (или группой разработчиков). Ситуации, когда разработку виртуального базового класса необходимо поручить независимому производителю, чрезвычайно редки, а разработчик нового базового класса не может внести изменения в существующую иерархию.
Виртуальное наследование влияет на те классы, которые происходят от виртуального базового класса впоследствии; оно не влияет на класс производный непосредственно.
Использование виртуального базового классаБазовый класс объявляется виртуальным при помощи ключевого слова virtual в списке наследования:
// порядок расположения ключевых слов public и virtual несуществен
class Raccoon : public virtual ZooAnimal { /* ... */ };
class Bear : virtual public ZooAnimal { /* ... */ };
Здесь класс ZooAnimal объявлен виртуальным базовым для классов Bear и Raccoon.
Спецификатор virtual заявляет о готовности совместно использовать единый экземпляр указанного базового класса в последующих производных классах. Нет никаких особых ограничителей на классы, используемые как виртуальные базовые классы.
Для наследования от класса, имеющего виртуальный базовый класс, не нужно ничего особенного:
class Panda : public Bear,
public Raccoon, public Endangered {
};
Здесь класс Panda наследует класс ZooAnimal через два своих базовых класса — Raccoon и Bear. Но поскольку эти классы происходят от класса ZooAnimal виртуально, у класса Panda есть только одна часть базового класса ZooAnimal.
Для базовых классов поддерживаются стандартные преобразованияОбъектом производного класса можно манипулировать как обычно, при помощи указателя или ссылки на базовый класс, хотя он и является виртуальным. Например, все следующие преобразования для базового класса объекта класса Panda вполне допустимы:
void dance(const Bear&);
void rummage(const Raccoon&);
ostream& operator<<(ostream&, const ZooAnimal&);
Panda ying_yang;
dance(ying_yang); // ok: передает объект Panda как Bear
rummage(ying_yang); // ok: передает объект Panda как Raccoon
cout << ying_yang; // ok: передает объект Panda как ZooAnimal
Видимость членов виртуальных базовых классовПоскольку виртуальному базовому классу соответствует только один совместно используемый внутренний объект, к членам объекта этого базового класса можно обратиться непосредственно и однозначно. Кроме того, если член виртуального базового класса переопределяется только в одной ветви наследования, к этому переопределенному члену класса можно обратиться непосредственно. Если член переопределяется больше чем одним базовым классом, то производный класс вообще должен определить собственную версию этого члена.
Предположим, например, что класс В определяет члены по имени x; класс D1 виртуально наследует класс В, как и класс D2; а класс D происходит от классов D1 и D2. Из области видимости класса D член x видим через оба своих базовых класса. Есть три возможности использовать член x через объект класса D:
• Если член x не будет определен ни в классе D1, ни в D2, то будет использован член класса В; никакой неоднозначности нет. Объект класса D содержит только один экземпляр члена x.
• Если x является членом класса В и одного (но не обоих) из классов D1 или D2, никакой неоднозначности снова нет: версия в производном классе имеет приоритет перед совместно используемым виртуальным базовым классом B.
• Если член x определяется и в классе D1, и в классе D2, то прямой доступ к этому члену неоднозначен.
Как и в иерархии с невиртуальным множественным наследованием, подобная неоднозначность лучше всего устраняется переопределением члена в производном классе.
Упражнения раздела 18.3.4Упражнение 18.28. Рассмотрим следующую иерархию класса. Можно ли в классе vmi обращаться к унаследованным членам без уточнения? Какие из них требуют полностью квалифицированных имен? Объясните, почему.
struct Base {
void bar(int); // по умолчанию открыты
protected:
int ival;
};
struct Derived1 : virtual public Base {
void bar(char); // по умолчанию открыты
void foo(char);
protected:
char cval;
};
struct Derived2 : virtual public Base {
void foo(int); // по умолчанию открыты