На уроке о множественном наследовании мы говорили о проблеме «алмаза смерти». На этом уроке мы продолжим эту тему.
Алмаз смерти
Код из того же урока, иллюстрирующий «алмаз смерти» (мы добавили еще конструкторы):
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.
На сколько помню я, вызов конструктора базового класса должен быть первым. Даже если нет, то лучше писать именно так, потому что последовательность именно такая.
Я так понимаю это нужно для того, чтобы не создавалась несколько
экземпляров родительских классов, каждый их которых относится к своему наследнику и не выделялась 2 раза память для родителя. Но интересно вот что, при выделении памяти на такой родительский объект, становиться ли она общей для 2-х дочерних классов?
Нет, это нужно, чтоб избежать ошибки неоднозначности.
Если ты в самом дочернем классе обратишься к переменной, которая есть в базовом классе, то к какой именно переменной из двух,
B::A::m или C::A::m ?
Т.к. m попадает и в B::A и в C::A (где A — базовый класс и для B и для C),
то нет однозначности, какую из этих 2 переменных использовать в наследнике D (его родители B и C).
Получишь что-то типа "Reference to m is ambiguous".
Вроде где то тут была ссылка на сайт майкрософт, где разбирались нюансы. Могли бы скинуть?
Огромнейшее спасибо за перевод!
Такой стиль изложения воспринимается гораздо проще, чем в учебнике.
Пожалуйста 🙂