Во-вторых, абстрактный класс PoweredDevice содержит виртуальный деструктор. Это резервирует в объекте место под указатель vptr. Остается 8 байт

В-третьих, класс Printer вводит собственную виртуальную функцию printDocument. В такой сложной иерархии классов не получается обходиться только одной таблицей виртуальных функций из-за сложных смещений, потому существует второй указатель vptr.. Остается 4 байта.

Наконец, последние 4 байта расходуются на еще один служебный указатель, который называется vbptr (virtual base class pointer), и представляет собой адрес массива, содержащего смещения (число байт) виртуальных базовых классов относительно местоположения указателя vbptr. В нашем случае, виртуальный базовый класс только один, потому массив будет содержать одну полезную запись. Если добавить в иерархию еще несколько виртуальных базовых классов, это дополнит данный массив, не вводя новых указателей в память объектов.

Графически это можно представить следующим образом:

 

В начале объекта Printer находится собственный указатель на таблицу виртуальных функций, добавленных данным классом (vptr_1). Далее следует указатель на массив смещений базовых классов (vbptr) в объекте относительно этого же указателя. Элемент с индексом 0 - это смещение самого класса Printer, т.е. на 4 байта назад. Элемент с индексом 1 - это уже смещение виртуального базового класса PoweredDevice относительно vbptr. Далее следует поле m_pagesPerMinute, за которым размещаются члены класса PoweredDevice. Первым из них является еще один указатель vptr для соответствующих классу PoweredDevice виртуальных функций.

Соответственно, любой код, который имея указатель или ссылку на объект класса Printer, при этом обращающийся к членам унаследованного виртуального базового класса PoweredDevice, делает следующие действия:

Printer * p = new Printer( 300, 15 );

std::cout << p->getNominalPower();

из адреса p извлекается указатель vbptr;

из указателя vbptr извлекается 2 смещения по индексу 1 и по индексу 0;

извлеченный смещения складываются, формируя смещение объекта PoweredDevice относительно начала объекта Printer;

выполняется вызов метода getNominalPower (если, конечно, он не будет встраиваться в месте вызова), и ему передается адрес this, равный адресу в указателе p + нужное смещение.

Как это очевидно из пояснений, наличие виртуальных базовых классов является довольно дорогостоящим и с точки зрения памяти, и с точки зрения производительности всех вызовов. Разумеется, такой тип наследования следует применять лишь в случаях, когда такое функциональное поведение является реально востребованным в задаче.

Внутренняя структура объекта Scanner в целом формируется аналогичным образом. Интерес представляет объект Copier, с размером 36 байт. Внутренняя структура данного объекта выглядит следующим образом:

 

Такой сложный объект состоит из 4 информационных полей (16 байт), 3 указателей vptr по каждому из базовых классов (12 байт) и 2 указателей vbptr на виртуальный базовый класс из промежуточных базовых классов (8 байт). Итого, 36 байт.

При внимательном чтении кода примера должен вызвать удивление выделенный код в списке инициализации конструктора Copier:

// Реализация конструктора

Copier::Copier ( int _nominalPower, int _scanDPI, int _pagesPerMinute )

// Вызов конструктора виртуального базового класса через всю иерархию!

: PoweredDevice( _nominalPower )

// Вызов конструкторов обычных базовых

, Scanner( _nominalPower, _scanDPI )

, Printer( _nominalPower, _pagesPerMinute )

{

}

Т.е., конструктор класса Copier, состоящих в двух уровнях иерархии наследования от класса PoweredDevice осуществляет вызов данного конструктора. При чем, это действие выполняется раньше, чем произойдут вызовы конструкторов Scanner и Printer, для которых общая часть виртуального базового класса должна быть уже инициализированной.

Подытожим обнаруженные нестыковки с ранее изученным материалом. Если в иерархии наследования имеется виртуальный базовый класс, то его местоположение в объекте определяется непосредственным конкретным классом, к которому он относится. Только самый производный класс иерархии (“most derived” class) знает точное местоположение виртуального базового. Местоположение может отличаться для каждого конкретного класса в иерархии. Чтобы промежуточные базовые классы могли работать с членами виртуального базового, самый производный класс должен обеспечить корректную инициализацию указателей vbptr в собственном конструкторе. Это можно увидеть при дизассебмлировании. Конструктор класса Copier содержит такой код, который устанавливает два указателя vbptr в нужное значение:

01378E72 mov eax,dword ptr [ebp-14h]

01378E75 mov dword ptr [eax+4],offset Copier::`vbtable' (139BE8Ch)

01378E7C mov eax,dword ptr [ebp-14h]

01378E7F mov dword ptr [eax+10h],offset Copier::`vbtable' (139BE80h)

01378E86 mov eax,dword ptr [ebp+8]

В то же время, класс Printer (и Scanner аналогично), конструкторы которых вызываются раньше Copier, содержат такой код:

01388923 mov eax,dword ptr [this]

01388926 mov dword ptr [eax+4],offset Printer::`vbtable' (139D840h)

Помимо сложной структуры, виртуальное наследование оказывает существенное влияние на порядок инициализации и уничтожения объектов. В приведенном примере порядок выполнения тел конструкторов участвующих классов для объекта Copier будет следующим, при этом конструктор виртуального базового класса вызовется первым и единственный раз:

 

PoweredDevice::PoweredDevice

Printer::Printer

Scanner::Scanner

Copier::Copier

Если будет создан объект класса Printer, его порядок конструирования такой:

PoweredDevice::PoweredDevice

Printer::Printer

Если будет создан объект класса Scanner, его порядок конструирования такой:

PoweredDevice::PoweredDevice

Scanner::Scanner

Возникает вопрос - если конструкторы классов Printer и Scanner вызывают конструктор класса PoweredDevice, то каким же образом достигается единственность его вызова при конструировании объекта Copier? Хитрость состоит в передаче в конструкторы классы иерархии еще одного неявного целого аргумента, означающего является ли класс текущего конструктора “самым производным”. Этот аргумент не увидеть нигде, кроме дизассебмлера. Предположим создается объект Copier. Код, который инициирует создание объекта, всегда знает конкретный тип, поскольку фактически его указывает, соответственно в этом случае передается 1:

Copier c( 500, 300, 15 );

Push 1

Push 0Fh

Push 12Ch

E push 1F4h

01302293 lea ecx,[ebp-34h]

01302296 call Copier::Copier (12E3311h)

 

 

Аналогично, если создать объект класса Printer, подобная единица будет неявно передана его конструктору:

Printer p( 300, 15 );

E223D push 1

E223F push 0Fh

E2241 push 12Ch

013E2246 lea ecx,[ebp-28h]

013E2249 call Printer::Printer (13C349Ch)

В то же время, когда объект Copier в собственном конструкторе вызывает конструктор класса Printer, вместо единицы передается 0, поскольку в таком случае Printer не является “самым производным” классом:

EC4 push 0

01378EC6 mov eax,dword ptr [ebp+10h]

EC9 push eax

01378ECA mov ecx,dword ptr [ebp+8]

ECD push ecx

01378ECE mov ecx,dword ptr [ebp-14h]

ED1 add ecx,0Ch

01378ED4 call Printer::Printer (137349Ch)

 

Этот аргумент используется в конструкторах Printer, Scanner и Copier таким образом, чтобы вызывать конструктор виртуального базового класса только при переданном значении 1 (т.е., когда текущий класс является “самым производным”). Это можно выразить следующим псевдокодом конструктора Printer:

Printer::Printer ( Printer * this,

Int _nominalpower,

Int _pagesPerMinute,

int mostDerived )

{

If ( mostDerived )

PoweredDevice::PoweredDevice( this + vbptr[ 0 ] + vbptr[ 1 ], _nominalPower );

this->m_pagesPerMinute = _pagesPerMinute;

}

Аналогично, конструктор класса Copier содержит нечто подобное:

Copier::Copier ( Copier * this,

Int _nominalpower,

Int _pagesPerMinute,

Int _scanDPI,

int mostDerived )

{

If ( mostDerived )

PoweredDevice::PoweredDevice( this + offset3, _nominalPower );

Printer::Printer( this + offset1, _nominalPower, _pagesPerMinute, 0 );

Scanner::Scanner( this + offset2, _nominalPower, _scanDPI, 0 );

}

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

 

Copier::~Copier

Scanner::~Scanner

Printer::~Printer

PoweredDevice::~PoweredDevice

Реализация виртуального базового наследования в других компиляторах, в частности в компиляторе GCC - основном компиляторе на платформе Linux, отличается иным размещением смещений виртуальных базовых классов. Указатели vbptr и дополнительные таблицы vbtable здесь вообще не используются. Вместо этого, смещения размещаются в таблице виртуальных функций по отрицательным индексам. Если словосочетание “отрицательные индексы” в массиве вызывает недоумение, следует вспомнить, что vptr является указателем, а значит может указывать на любой из элементов массива. Разумеется, нумерация ячеек в самой таблице начинается с 0, однако в начале таблицы помещают смещения виртуальных базовых классов, а только за ними - адреса виртуальных функций. Указатель vptr устанавливается таким образом, чтобы по нулевому смещению размещался адрес первой виртуальной функции. Соответственно, все предыдущие данные становятся доступны по отрицательным индексам.

 

Такая структура является более компактной по сравнению с реализацией в компиляторе Microsoft, несмотря на более сложную для восприятия схему реализации. Для объектов Printer и Scanner экономия составляет 1 указатель на каждом объекта, а для Copier - целых 2 указателя.

 

 

39. Механизм RTTI - назначение, особенности применения. Структура std::type_info, оператор typeid для выражений и типов.

 

RTTI механизм С++, предназначенный для определения типов во время выполнения программ. Его прямое применение носит довольно узко специализированный характер, однако на его основе функционируют такие важные механизмы языка, как оператор dynamic_cast и обработка исключений

Собственно, информация о типах, доступная программам для обработки, заключена в рамки структуры std::type_info из стандартной библиотеки, определение которой становится доступным после включения заголовочного файла <typeinfo>. Такая структура создается компилятором для всех использованных в программе типов данных. Сама структура довольно проста и содержит следующие открытые методы, доступные для программной обработки:

 

● Метод name(), возвращающий строку, представляющую название типа. Формат названия стандартом языка никак не определяется, и зависит от используемой среды разработки. Для простых случаев это название обычно соответствует названию типа в коде.

 

● Перегруженные операторы сравнения на равенство == и неравенство !=. Позволяют сравнивать два объекта type_info между собой для сопоставления. Реализация не гарантирует, что в программе для одного и того же типа будет существовать ровно 1 объект type_info, соответственно сравнивать два объекта type_info по адресу не является корректным, следует пользоваться именно операторами сравнения.

 

● Метод before, принимающий ссылку на другой объект std::type_info, и возвращающий bool, если первый тип условно “раньше” второго по порядку. Конкретный критерий для сравнения типов стандартом языка не оговаривается, реализация в праве использовать любой однозначный критерий. Этот метод можно использовать для сортировки объектов-типов, а также для помещения указателья на объект std::type_info в качестве ключа в контейнеры на основе бинарных деревьев (std::map, std::set).

 

● Объекты такой структуры копировать нельзя, реализация должна запрещать конструктор копий и оператор присвоения.

 

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

#include "date.hpp"

#include <iostream>

#include <string>

#include <typeinfo>

Int main ()

{

const std::type_info & intTypeInfo = typeid( int );

std::cout << intTypeInfo.name() << std::endl;

const std::type_info & dateTypeInfo = typeid( Date );

std::cout << dateTypeInfo.name() << std::endl;

const std::type_info & stringTypeInfo = typeid( std::string );

std::cout << stringTypeInfo.name() << std::endl;

}

 

Также, оператор typeid можно применить к любому выражению :

#include "date.hpp"

 

#include <iostream>

#include <string>

#include <typeinfo>

 

intmain ()

{

conststd::type_info & typeInfo1 = typeid( 2 + 5.3 );

std::cout << typeInfo1.name() << std::endl;

 

conststd::type_info & typeInfo2 = typeid( Date().GetMonth() );

std::cout << typeInfo2.name() << std::endl;

 

conststd::type_info & typeInfo3 = typeid( & std::string::substr );

std::cout << typeInfo3.name() << std::endl;

}

Существует два основных способа работы оператора typeid:

● статический - подстановка во время компиляции по указанному типу / выражению;

● полиморфный - подстановка во время выполнения на основе информации о фактическом типе, даже если объект скрывается за указателем или ссылкой на базовый класс.