Множественное наследование классов с повторяющимся базовым. Синтаксис, структура в памяти, особенности применения и реализации

 

Прямое наследование повторяющихся базовых классов языком не допускается;

class A {};

class B : public A, public A {};

error C2500: 'B' : 'A' is already a direct base clas

Однако нечто подобное возможно косвенным образом, если производный класс наследуется от двух и более промежуточных, у которых имеется общий базовый класс:

 

Class Base

{

Int m_x;

public:

virtual void f () { … }

int getX () const { return m_x; }

};

Class Middle1

: public Base

{

Int m_y;

public:

virtual void g () { … }

};

Class Middle2

: public Base

{

Int m_z;

public:

virtual void h () { … }

};

 

Class Derived

: public Middle1, public Middle2

{

Int a;

public:

void f () override { … }

void g () override { … }

void h () override { … }

};

 

Таким образом, в объекте Derived содержимое базового класса Base представлено дважды - по каждой из веток иерархии. Внутреннее представление объекта выглядит следующим образом:

Ниже представлено соотношение размеров классов, участвующих в данной иерархии:

 

Размер объекта Derived определяется суммарным размером двух базовых классов + размер собственного поля. Т.е., в этот размер дважды входит размер содержимого класса Base. В свою очередь размер базовых классов определяется наличием двух информационных полей, одно из которых унаследовано из Base, + указателя vptr. При переопределении метода f в классе Derived, замещаются реализации по обеим версиям класса Base, в связи с чем в таблице виртуальных функций по ветке Middle1 используется непосредственный указатель на метод, а по ветке MIddle2 - корректирующий на 12 байт влево объект-thunk.

Интересно, что при такой структуре создается неоднозначность преобразования вверх по иерархии при попытке перейти на повторяющийся базовый класс:

Derived d;

Base * b = & d;

error C2594: 'initializing' : ambiguous conversions from 'Derived *' to 'Base *'

Неоднозначность обуславливается тем, что содержимое Base представлено в Derived дважды, и не понятно какой именно из этих экземпляров следует взять. Преобразование следует выполнить четко уточнив намерение через промежуточный базовый класс:

Derived d;

Middle1 * m1 = & d;

Base * b = m1;

либо:

Derived d;

Middle1 * m2 = & d;

Base * b = m2;

 

Разумеется, получаемый в итоге адрес будет различаться на соответствующее размеру класса Middle1 смещение. Из такой неоднозначности вытекает строжайший запрет на преобразование вниз по иерархии от класса Base к классу Derived напрямую, что чревато практически 100% ошибкой.

Прямое обращение к члену повторяющегося базового класса также создает неоднозначность:

Derived d;

D.getX();

Для разрешения необходимо указывать квалифицированные имена промежуточных базовых классов:

Derived d;

d.Middle1::getX();

Либо следует использовать директивы using:

Class Derived

: public Middle1, public Middle2

{

...

public:

...

using Middle1::getX;

};

 

Порядок вызова конструкторов в данной иерархии соответствует ожидаемому, конструктор класса Base будет вызван дважды - каждый раз на разных частях результирующего объекта:

 

Base::Base

Middle1::Middle1

Base::Base (смещенный this на начало Middle2)

Middle2::Middle2

Derived::Derived

Деструкторы продемонстрируют поведение с точностью до наоборот:

Derived::~Derived

Middle2::~Middle2

Base::~Base (смещенный this на начало Middle2)

Middle1::~Middle1

Base::~Base

 

38. Виртуальные базовые классы. Синтаксис, структура в памяти, особенности применения и реализации. Понятие “самого производного” класса и его роль в организации работы виртуальных базовых классов.

 

Внутренняя структура объектов в данной иерархии достаточно сложна. Поскольку только самый производный класс (т.е. класс, объект которого фактически создается в данный момент) может знать полный набор всех междуклассовых отношений, то именно самый производный класс решает в каком месте в результирующем объекте расположить содержимое виртуального базового. Кроме того, базовые классы, не имея возможности определить местоположение виртуального базового класса самостоятельно, должны опираться на некоторые дополнительные данные, позволяющие узнать отступ от начала объекта до начала содержимого виртуального базового класса. Т.е., если взять, например, класс Printer, и создать объект Printer, то местоположение содержимого класса PoweredDevice в таком объекте будет отличаться от местоположения в объекте Copier, несмотря на то, что Copier - это подвид Printer.

 

Существует 2 типовые схемы реализации виртуального наследования, различные между собой - одна из реализаций применяется в компиляторе компании Microsoft, другая - в компиляторе GCC. Начнем с компилятора Microsoft.

Предположим, создается конкретный объект класса Printer. Отладчик показывает его размер как 24 байта (VisualStudio 2010, 32-битный режим, настройки по умолчанию). Что же входит в этот размер? Считаем размер информационных полей:

m_nominalPower (4 байта);

m_turned (1 байт);

m_pagesPerMinute (4 байта).

 

Из этого расчета выходит, что объект должен содержать 9 байт, а ни как не 24. На что же уходит еще 15 байт?

Во-первых, компилятор применяет схемы выравнивания структур по границе (настройка по умолчанию = 4 байта), поэтому поле m_turned занимает не 1 байт, а 4. Остается 12 байт.