На уроці про множинне спадкування ми говорили про проблему «алмаз смерті». На цьому уроці ми продовжимо цю тему.
Алмаз смерті
Код з того ж уроку, який ілюструє “алмаз смерті” (ми додали ще конструктори):
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
class PoweredDevice { public: PoweredDevice(int power) { std::cout << "PoweredDevice: " << power << '\n'; } }; class Scanner: public PoweredDevice { public: Scanner(int scanner, int power) : PoweredDevice(power) { std::cout << "Scanner: " << scanner << '\n'; } }; class Printer: public PoweredDevice { public: Printer(int printer, int power) : PoweredDevice(power) { std::cout << "Printer: " << printer << '\n'; } }; class Copier: public Scanner, public Printer { public: Copier(int scanner, int printer, int power) : Scanner(scanner, power), Printer(printer, power) { } }; |
Хоча ви можете очікувати, що діаграма спадкування буде наступна:

Насправді, це не так. Якщо ви створите об’єкт класу Copier, то отримаєте дві копії класу PoweredDevice: одну від Printer і одну від Scanner.
Діаграма наступна:

Розглянемо приклад в коді:
|
1 2 3 4 |
int main() { Copier copier(1, 2, 3); } |
Результат:
PoweredDevice: 3
Scanner: 1
PoweredDevice: 3
Printer: 2
Як ви бачите, PoweredDevice створюється двічі. Іноді так і потрібно, а іноді потрібно, щоб була одна копія PoweredDevice: загальна як для Scanner, так і для Printer.
Віртуальні базові класи
Щоб зробити батьківський (базовий) клас загальним, використовується ключове слово virtual в рядку оголошення дочірнього класу. Віртуальний базовий клас — це клас, об’єкт якого є загальним для використання всіма дочірніми класами. Ось приклад (без конструкторів для простоти) створення загального батьківського класу:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
class PoweredDevice { }; class Scanner: virtual public PoweredDevice { }; class Printer: virtual public PoweredDevice { }; class Copier: public Scanner, public Printer { }; |
Тепер, при створенні класу Copier, ми отримаємо тільки одну копію PoweredDevice, яка буде спільною як для Scanner, так і для Printer.
Виникає питання: «Якщо Scanner і Printer спільно використовують батьківський клас PoweredDevice, то хто відповідальний за його створення?». Виявляється, що Copier. Конструктор Copier відповідає за створення об’єкта PoweredDevice. Це один з тих випадків, коли дочірньому класу дозволено викликати конструктор батьківського класу, який не є його безпосереднім батьком:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
#include <iostream> class PoweredDevice { public: PoweredDevice(int power) { std::cout << "PoweredDevice: " << power << '\n'; } }; class Scanner: virtual public PoweredDevice // примітка: PoweredDevice тепер віртуальний базовий клас { public: Scanner(int scanner, int power) : PoweredDevice(power) // цей рядок необхідний для створення об'єктів класу Scanner, але в цій програмі він ігнорується { std::cout << "Scanner: " << scanner << '\n'; } }; class Printer: virtual public PoweredDevice // примітка: PoweredDevice тепер віртуальний базовий клас { public: Printer(int printer, int power) : PoweredDevice(power) // цей рядок необхідний для створення об'єктів класу Printer, але в цій програмі він ігнорується { std::cout << "Printer: " << printer << '\n'; } }; class Copier: public Scanner, public Printer { public: Copier(int scanner, int printer, int power) : Scanner(scanner, power), Printer(printer, power), PoweredDevice(power) // побудова PoweredDevice виконується тут { } }; int main() { Copier copier(1, 2, 3); } |
Результат виконання програми:
PoweredDevice: 3
Scanner: 1
Printer: 2
Тут вже PoweredDevice створюється тільки один раз.
Обговоримо кілька деталей.
По-перше, віртуальні базові класи завжди створюються перед невіртуальними базовими класами, що забезпечує побудову всіх базових класів до побудови їх дочірніх класів.
По-друге, конструктори Scanner і Printer як і раніше викликають конструктор PoweredDevice. При створенні об’єкта Copier ці виклики конструктора просто ігноруються, тому що саме Copier відповідає за створення PoweredDevice, а не Scanner чи Printer. Однак, якщо б ми створювали об’єкти Scanner або Printer, то ці конструктори викликалися б і застосовувалися звичайні правила спадкування.
По-третє, якщо клас, стаючи дочірнім, наслідує один або кілька класів, які, в свою чергу, мають віртуальні батьківські класи, то “найдочірніший” клас відповідає за створення віртуального батьківського класу. У вищенаведеній програмі Copier наслідує Printer і Scanner, які обидва мають загальний віртуальний батьківський клас PoweredDevice. Copier, “найдочірніший” клас, відповідає за створення PoweredDevice. Це працює навіть в разі одиночного спадкування: коли Copier наслідує тільки Printer, а Printer віртуально наслідує PoweredDevice, то Copier як і раніше відповідає за створення PoweredDevice.
