Виртуальное наследование
Часть раздела Объектно-ориентированное программирование путешествия по C++ на Coddy — урок 54 из 104.
Проблема ромба возникает, когда класс наследуется от двух классов, имеющих общий базовый класс. Без специальной обработки производный класс получает две копии общего базового класса, что приводит к неоднозначности и нерациональному использованию памяти.
class Animal {
public:
int age;
};
class Mammal : public Animal {};
class Bird : public Animal {};
class Bat : public Mammal, public Bird {};
Bat b;
b.age = 5; // Ошибка: неоднозначность — какой именно 'age'?Класс Bat содержит два отдельных подобъекта Animal: один через Mammal и один через Bird. Это создает ромбовидную диаграмму наследования, которая и дала название этой проблеме.
Виртуальное наследование решает эту проблему, гарантируя существование только одной копии общего базового класса. Добавьте ключевое слово virtual при наследовании от общего базового класса:
class Animal {
public:
int age;
Animal(int a = 0) : age(a) {}
};
class Mammal : virtual public Animal {
public:
Mammal(int a = 0) : Animal(a) {}
};
class Bird : virtual public Animal {
public:
Bird(int a = 0) : Animal(a) {}
};
class Bat : public Mammal, public Bird {
public:
Bat(int a) : Animal(a), Mammal(a), Bird(a) {}
};
Bat b(5);
b.age = 10; // Работает! Существует только один 'age'Обратите внимание, что Bat должен напрямую инициализировать Animal в своём конструкторе. При виртуальном наследовании самый производный класс отвечает за создание виртуального базового класса, независимо от промежуточных классов.
Задание
ЛегкоДавайте создадим систему управления сотрудниками, которая демонстрирует, как виртуальное наследование решает проблему ромба (diamond problem). Вы создадите иерархию, в которой TeamLead наследуется как от Developer, так и от Manager, которые оба имеют общий базовый класс Employee.
Вы организуете свой код в четырех файлах:
Employee.h: Определите общий базовый классEmployeeс:- Защищенными (protected) полями
std::string nameиint id - Конструктором, который принимает оба значения и выводит:
Employee [<name>] hired with ID <id> - Публичным методом
getInfo(), который выводит:Employee: <name> (ID: <id>) - Виртуальным деструктором, который выводит:
Employee [<name>] record closed
- Защищенными (protected) полями
Developer.h: Определите классDeveloper, который использует виртуальное публичное наследование отEmployee:- Защищенное поле
std::string language - Конструктор, который принимает имя, id и язык — передает имя и id в
Employee, сохраняет язык и выводит:Developer [<name>] specializes in <language> - Публичный метод
code(), который выводит:<name> is coding in <language> - Деструктор, который выводит:
Developer [<name>] signed off
- Защищенное поле
Manager.h: Определите классManager, который использует виртуальное публичное наследование отEmployee:- Защищенное поле
int teamSize - Конструктор, который принимает имя, id и размер команды — передает имя и id в
Employee, сохраняет размер команды и выводит:Manager [<name>] leads a team of <teamSize> - Публичный метод
manage(), который выводит:<name> is managing <teamSize> people - Деструктор, который выводит:
Manager [<name>] stepped down
- Защищенное поле
main.cpp: Считайте четыре входных значения (каждое на отдельной строке):- Имя (string)
- ID сотрудника (integer)
- Язык программирования (string)
- Размер команды (integer)
Определите класс
TeamLead, который публично наследуется как отDeveloper, так и отManager:- Конструктор, который принимает все четыре параметра и должен напрямую инициализировать
Employee(виртуальную базу), а затемDeveloperиManager - Конструктор должен выводить:
TeamLead [<name>] ready to lead and code! - Метод
showRole(), который вызываетgetInfo(),code()иmanage()именно в таком порядке - Деструктор, который выводит:
TeamLead [<name>] promoted out
Создайте объект
TeamLeadвнутри блока (scope), вызовитеshowRole(), а затем позвольте ему выйти из области видимости. После блока выведите:Organization restructured!
Например, при входных данных Alice, 101, C++ и 5:
Employee [Alice] hired with ID 101
Developer [Alice] specializes in C++
Manager [Alice] leads a team of 5
TeamLead [Alice] ready to lead and code!
Employee: Alice (ID: 101)
Alice is coding in C++
Alice is managing 5 people
TeamLead [Alice] promoted out
Manager [Alice] stepped down
Developer [Alice] signed off
Employee [Alice] record closed
Organization restructured!Обратите внимание, что вызывается только один конструктор Employee и один деструктор Employee — виртуальное наследование гарантирует существование только одной копии общего базового класса. TeamLead должен напрямую инициализировать Employee, потому что при виртуальном наследовании за создание виртуальной базы отвечает самый производный класс.
Шпаргалка
Проблема ромба (diamond problem) возникает, когда класс наследуется от двух классов, имеющих общий базовый класс, что приводит к созданию двух копий базового класса и вызывает неоднозначность.
class Animal {
public:
int age;
};
class Mammal : public Animal {};
class Bird : public Animal {};
class Bat : public Mammal, public Bird {};
Bat b;
b.age = 5; // Error: ambiguous - which 'age'?Виртуальное наследование решает проблему ромба, гарантируя существование только одной копии общего базового класса. Используйте ключевое слово virtual при наследовании:
class Animal {
public:
int age;
Animal(int a = 0) : age(a) {}
};
class Mammal : virtual public Animal {
public:
Mammal(int a = 0) : Animal(a) {}
};
class Bird : virtual public Animal {
public:
Bird(int a = 0) : Animal(a) {}
};
class Bat : public Mammal, public Bird {
public:
Bat(int a) : Animal(a), Mammal(a), Bird(a) {}
};
Bat b(5);
b.age = 10; // Works! Only one 'age' existsПри виртуальном наследовании наиболее производный класс должен напрямую инициализировать виртуальный базовый класс в своем конструкторе, независимо от промежуточных классов.
Попробуйте сами
#include <iostream>
#include <string>
#include "Developer.h"
#include "Manager.h"
using namespace std;
// TODO: Определите класс TeamLead, который публично наследуется как от Developer, так и от Manager
// Помните: при виртуальном наследовании TeamLead должен напрямую инициализировать Employee (виртуальный базовый класс)
class TeamLead : public Developer, public Manager {
public:
// TODO: Реализуйте конструктор, который принимает name, id, language и teamSize
// Должен инициализировать: сначала Employee (виртуальная база), затем Developer, затем Manager
// Должен выводить: TeamLead [<name>] ready to lead and code!
TeamLead(const std::string& name, int id, const std::string& language, int teamSize)
: Employee(name, id),
Developer(name, id, language),
Manager(name, id, teamSize) {
// TODO: Выведите сообщение конструктора
}
// TODO: Реализуйте метод showRole()
// Должен вызывать getInfo(), code() и manage() именно в таком порядке
void showRole() {
// TODO: Вызовите эти три метода
}
// TODO: Реализуйте деструктор
// Должен выводить: TeamLead [<name>] promoted out
~TeamLead() {
// TODO: Выведите сообщение деструктора
}
};
int main() {
// Чтение входных данных
string name;
int id;
string language;
int teamSize;
getline(cin, name);
cin >> id;
cin.ignore();
getline(cin, language);
cin >> teamSize;
// TODO: Создайте объект TeamLead внутри области видимости блока
// Вызовите showRole(), затем позвольте объекту выйти из области видимости
{
// TODO: Создайте TeamLead и вызовите showRole()
}
// Вывод финального сообщения после блока
cout << "Organization restructured!" << endl;
return 0;
}
В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.
Все уроки раздела Объектно-ориентированное программирование
1Основы ООП
Внешние файлыСборка и компиляция C++Заголовочные файлы и файлы исходного кодаПространства имен и область видимостиВведение в ООП на C++Классы и объектыУказатель 'this'Методы (функции-члены)Атрибуты (члены данных)Основы конструкторов и деструкторовИтоги — Простой калькулятор4Свойства классов
Члены экземпляра против статическихГеттеры и сеттерыКонстантные функции-членыКлючевое слово mutableСтатические методы и переменныеДружественные функции и классыИтоги — Менеджер банковских счетов7Наследование
Основы наследованияУровни доступа при наследованииПорядок вызова конструкторов и деструкторовПереопределение методовВиртуальные функции и VTableМножественное наследованиеВиртуальное наследованиеИтоги — Иерархия сотрудников10Обзор STL
Обзор и философия STLКонтейнеры STLИтераторыАлгоритмы STLФункторы и лямбда-выраженияИтоги: частота слов13Паттерны проектирования. Часть 1
Введение в паттерны проектированияПаттерн SingletonFactory и Abstract FactoryПаттерн BuilderПаттерн ObserverПаттерн Strategy2Управление памятью
Стек и кучаУказатели и ссылкиДинамическая память (new/delete)Умные указатели в C++RAII в C++Итоги — Менеджер динамического массива5Инкапсуляция
Спецификаторы доступа в C++Спецификаторы доступа: подробный разборСокрытие данныхStruct против ClassВложенные и внутренние классыИтоги — Система учета студентов8Полиморфизм
Полиморфизм компиляции и времени выполненияПерегрузка функцийВиртуальные функции: повторениеЧистые виртуальные функцииАбстрактные классыПроектирование интерфейсов на C++Dynamic Casting и RTTIИтоги: Калькулятор фигур11Продвинутые концепции ООП
Композиция против наследованияМиксины через CRTPИдиома PimplСтирание типовEnum Classes и строгая типизацияОбработка исключений в ООППользовательские иерархии исключений14Паттерны проектирования. Часть 2
Паттерн КомандаПаттерн АдаптерПаттерн ДекораторПаттерн Шаблонный методПаттерн СостояниеПаттерн КомпоновщикRAII как паттерн3Конструкторы и деструкторы
Конструктор по умолчаниюПараметризованный конструкторКонструктор копированияКонструктор перемещенияСписки инициализации конструктораДелегирующие конструкторыГлубокое погружение в деструкторыПравило трех / пяти / нуляПовторение — класс String6Перегрузка операторов
Введение в перегрузку операторовПерегрузка арифметических операторовПерегрузка операторов сравненияОператоры потоковПерегрузка оператора присваиванияПерегрузка операторов [] и ()Операторы преобразования типовИтоги — класс Matrix9Шаблоны
Шаблоны функцийШаблоны классовСпециализация шаблоновВариативные шаблоныОсновы SFINAE и Type TraitsИтоги — Обобщенный контейнер