18.8 – Виртуальные базовые классы
В последней главе, в уроке «17.9 – Множественное наследование», мы закончили разговор на «проблеме ромба» (или англоязычный термин – diamond problem). В этом разделе мы продолжим это обсуждение.
Примечание. Этот раздел представляет сложную тему, и при желании его можно пропустить или отложить на позже.
Проблема ромба
Вот наш пример из предыдущего урока (с конструкторами), иллюстрирующий проблему ромба:
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
. Диаграмма наследования будет иметь следующую структуру:
Мы можем создать небольшой пример, который продемонстрирует это в действии:
int main()
{
Copier copier{ 1, 2, 3 };
return 0;
}
Этот код дает следующий результат:
PoweredDevice: 3
Scanner: 1
PoweredDevice: 3
Printer: 2
Как видите, PoweredDevice
создался дважды.
Хотя это часто желательно, в других случаях может потребоваться, чтобы и частью Scanner
, и частью Printer
использовалась только одна копия PoweredDevice
.
Виртуальные базовые классы
Чтобы использовать базовый класс совместно, просто вставьте ключевое слово virtual
в список наследования производного класса. Это создает так называемый виртуальный базовый класс, что означает, что существует только один базовый объект. Этот базовый объект используется всеми объектами в дереве наследования и создается только один раз. Вот пример (для простоты без конструкторов), показывающий, как использовать ключевое слово virtual
для создания общего базового класса:
class PoweredDevice
{
};
class Scanner: virtual public PoweredDevice
{
};
class Printer: virtual public PoweredDevice
{
};
class Copier: public Scanner, public Printer
{
};
Теперь, когда вы создаете объект класса Copier
, вы получите только одну копию PoweredDevice
в объекте Copier
, которая будет использоваться как частью Scanner
, так и частью Printer
.
Однако это приводит к еще одной проблеме: если Scanner
и Printer
используют общий базовый класс PoweredDevice
, кто несет ответственность за его создание? Как оказалось, ответ – Copier
. За создание PoweredDevice
отвечает конструктор Copier
. Следовательно, это единственный раз, когда классу Copier
разрешено напрямую вызывать конструктор, не являющийся непосредственно родительским:
#include <iostream>
class PoweredDevice
{
public:
PoweredDevice(int power)
{
std::cout << "PoweredDevice: " << power << '\n';
}
};
// обратите внимание: PoweredDevice теперь является виртуальным базовым классом
class Scanner: virtual public PoweredDevice
{
public:
Scanner(int scanner, int power)
// эта строка требуется для создания объектов Scanner,
// но в данном случае игнорируется
: PoweredDevice{ power }
{
std::cout << "Scanner: " << scanner << '\n';
}
};
// обратите внимание: PoweredDevice теперь является виртуальным базовым классом
class Printer: virtual public PoweredDevice
{
public:
Printer(int printer, int power)
// эта строка требуется для создания объектов Printer,
// но в данном случае игнорируется
: PoweredDevice{ power }
{
std::cout << "Printer: " << printer << '\n';
}
};
class Copier: public Scanner, public Printer
{
public:
Copier(int scanner, int printer, int power)
: PoweredDevice{ power }, // PoweredDevice создается здесь
Scanner{ scanner, power }, Printer{ printer, power }
{
}
};
На этот раз наш предыдущий пример:
int main()
{
Copier copier{ 1, 2, 3 };
return 0;
}
дает результат:
PoweredDevice: 3
Scanner: 1
Printer: 2
Как видите, PoweredDevice
создается только один раз.
Есть несколько деталей, которые стоит упомянуть.
Во-первых, виртуальные базовые классы всегда создаются перед невиртуальными базовыми классами, что гарантирует создание всех базовых классов до их производных классов.
Во-вторых, обратите внимание, что у конструкторов Scanner
и Printer
всё еще есть вызовы конструктора PoweredDevice
. При создании экземпляра Copier
эти вызовы конструктора просто игнорируются, потому что за создание PoweredDevice
отвечает Copier
, а не Scanner
или Printer
. Однако если бы мы создавали экземпляр Scanner
или Printer
, эти вызовы конструктора были бы использованы, и были бы применены обычные правила наследования.
В-третьих, если класс наследует один или несколько классов, имеющих виртуальных родителей, за создание виртуального базового класса отвечает наиболее производный класс. В этом случае Copier
наследует Printer
и Scanner
, оба из которых имеют виртуальный базовый класс PoweredDevice
. Copier
, самый производный класс, отвечает за создание PoweredDevice
. Обратите внимание, что это верно даже в случае одиночного наследования: если Copier
унаследован только от Printer
, а Printer
был унаследован от PoweredDevice
, Copier
по-прежнему отвечает за создание PoweredDevice
.
В-четвертых, все классы, наследующие виртуальный базовый класс, будут иметь виртуальную таблицу, даже если в противном случае у них ее обычно не было бы, и, таким образом, размер экземпляров этих классов будет больше на размер указателя.
Поскольку Scanner
и Printer
фактически являются производными от PoweredDevice
, Copier
будет только с одним подобъектом PoweredDevice
. И Scanner
, и Printer
должны знать, как найти этот единственный подобъект PoweredDevice
, чтобы иметь доступ к его членам (потому что, в конце концов, они являются производными от него). Обычно это делается с помощью магии виртуальной таблицы (которая, по сути, сохраняет смещение от каждого подкласса к подобъекту PoweredDevice
).