Полиморфная кухня

3. Множественное и виртуальное наследование

В первой части серии мы рассмотрели основы наследования и полиморфизма в языке C++, во второй рассмотрели механизм, благодаря которому всё это работает. Идём дальше: создадим более замысловатую иерархию.

Сегодня реализуем следующую иерархию. Базовым классом будет класс Cmyk, реализующий цветовую схему CMYK. Класс CmykGy будет содержать дополнительный серый, класс CmykLcLm — дополнительные светло-синий и светло-малиновый. Класс CmykGyLcLmLg внизу иерархии будет содержать все упомянутые дополнительные цвета (серый, светло-синий, светло-малиновый), а также светло-серый.

 Cmyk
 ┌──────────────┴──────────────┐
 CmykGy CmykLcLm 
 └──────────────┬──────────────┘
 CmykGyLcLmLg

Соответствующий код очень простой.

class Cmyk
 {
 public:
 virtual ~Cmyk()
 {
 }
 
 virtual void print() const
 {
 std::cout << "CMYK." << std::endl;
 }
 
 protected:
 unsigned char cyan;
 unsigned char magenta;
 unsigned char yellow;
 unsigned char black;
 };
 
 
 
 class CmykGy : public Cmyk
 {
 public:
 virtual void print() const
 {
 std::cout << "CMYK + gray." << std::endl;
 }
 
 protected:
 unsigned char gray;
 };
 
 
 
 class CmykLcLm : public Cmyk
 {
 public:
 virtual void print() const
 {
 std::cout << "CMYK + light cyan + light magenta." << std::endl;
 }
 
 protected:
 unsigned char lightCyan;
 unsigned char lightMagenta;
 };
 
 
 
 class CmykGyLcLmLg : public CmykGy, public CmykLcLm
 {
 public:
 virtual void print() const
 {
 std::cout << "CMYK + gray " 
 << "+ light cyan + light magenta + light gray." << std::endl;
 }
 
 protected:
 unsigned char lightGray;
 };

Как было сказано, производный класс включает все поля, которые содержатся в базовом. Получается, что класс CmykGyLcLmLg содержит в себе поля класса Cmyk дважды! Это не только не соответствует нашим целям, но и расходует память. Такое явление назвается проблемой ромбовидного наследования (diamond problem). (Хотя заметим, что в какой-то другой ситуации такое дублирование может иметь смысл.)

Слегка модифицируем объявления классов CmykGy и CmykLcLm (вы можете скачать получившийся исходный файл).

...
 class CmykGy : public virtual Cmyk
 {
 ...
...
 class CmykLcLm : public virtual Cmyk
 {
 ...

Схема принципиально меняется.

При виртуальном наследовании базовый подобъект (давайте назовём его так) хранится в единственном экземпляре (показано стрелками). Но перед этим помещается указатель на него. В случае классов, которые наследуются непосредственно от базового (CmykGy и CmykLcLm), всё более-менее наглядно.

Подробно рассмотрим, как формируется структура класса CmykGyLcLmLg.

  1. CmykGy — это Cmyk. Действительно, первое же поле — указатель на Cmyk (пусть и добавляется один уровень косвенности).
  2. Каждый производный класс включает в себя базовый (все базовые). Можно увидеть, что подобъекты CmykGy и CmykLcLm поочердно включены в производный объект. Сам базовый подобъект располагается далее, он доступен по любому из указателей.
  3. Производный класс может добавлять свои поля.

Заключение

Мы рассмотрели, как разрабатывать классы, которые получаются в результате наследования от нескольких классов, имеющих общий базовый. Однако детали реализации механизма виртуального наследования могут отличаться даже между различными версиями одного и того же компилятора.

Исходные файлы

  1. cmyk.cpp
27 декабря 2016 · наследование и полиморфизм · программирование