М. Бен-Ари - Языки программирования. Практический сравнительный анализ (2000) (1160781), страница 41
Текст из файла (страница 41)
virtual void set_speed(int); // Заместить виртуальную подпрограмм private:
int reentry_speed;
};
Space_Plane_Data sp;
update(sp, 2000,30000);
файл, содержащий определение для update, не нужно перекомпилировать, даже если а) новая подпрограмма заместила set_speed и б) значение формального параметра d в update содержит дополнительное поле reentry_speed.
Когда используется динамический полиморфизм?
Давайте объявим базовый класс с виртуальной подпрограммой и обычной невиртуальной подпрограммой и породим класс, который добавляет дополнительное поле и дает новые объявления для обеих подпрограмм:
class Base_Class {
private:
int Base_Field;
public:
virtual void virtual_proc();
void ordinary_proc();
};
class Derived_Class : public Base_Class {
private:
int Derived_Field;
public:
virtual void virtual_proc();
void ordnary_proc(); };
Затем объявим экземпляры классов в качестве переменных. Присваивание значения производного класса переменной из базового класса разрешено:
Base_Class Base_0bject;
Derived_Class Derived_Object;
if (...) Base_0bject = Derived_Object;
потому что производный объект является базовым объектом (плюс дополнительная информация), и при присваивании дополнительная информация может игнорироваться (см. рис. 14.3).
Более того, вызов подпрограммы (виртуальной или не виртуальной) однозначный, и компилятор может использовать статическое связывание:
Base_0bject .virtual_proc();
Base_Object.ordinary_proc();
Derived_0bject.virtual_proc();
Derived_0bject.ordinary_proc();
Предположим, однако, что используется косвенность, и указатель на производный класс присвоен указателю на базовый класс:
Base_Class* Base_Ptr = new Base_Class;
Derived_Class* Derived_Ptr = new Derived_Class;
if (...) Base_Ptr = Derived_Ptr;
В этом случае семантика другая, так как базовый указатель ссылается на полный производный объект без каких-либо усечений (см. рис. 14.4). При реализации не возникает никаких проблем, потому что мы принимаем, что все указатели представляются одинаково независимо от указуемого типа.
Важно обратить внимание на то, что после присваивания указателя компилятор больше не имеет никакой информации относительно типа указуемого объекта. Таким образом, у него нет возможности привязать вызов
Base_Ptr- >virtual_proc();
к правильной подпрограмме, и следует выполнить динамическую диспетчеризацию. Аналогичная ситуация возникает, когда используется ссылочный параметр, как было показано выше.
Эта ситуация может внести путаницу, так как программисты обычно не делают различия между переменной и указуемым объектом. После следующих операторов:
inti1 = 1;
int i2 = 2;
int *p1 = &i1; // p1 ссылается на i1
int *p2 = &i2; // p2 ссылается на i2
p1 = p2; // p1 также ссылается на i2
i1 = i2; // i1 имеет то же самое значение, что и i2
вы ожидаете, что i1 == i2 и *р1 ==*р2; это, конечно, правильно, пока типы в точности совпадают, но это неверно для присваивания производного класса базовому классу из-за усечения. При использовании наследования вы должны помнить, что указуемый объект может иметь тип, отличный от типа указуемого объекта в объявлении указателя.
Есть одна западня в семантике динамического полиморфизма языка C++: если вы посмотрите внимательно, то заметите, что обсуждение касалось диспетчеризации, относящейся к замещенной виртуальной подпрограмме. Но в классе могут также быть и обычные подпрограммы, которые замещаются:
Base_Ptr = Derived_Ptr;
Base_Ptr->virtual_proc(); // Диспетчеризуется по указанному типу
Base_Ptr->ordinary_proc(); // Статическое связывание с базовым типом!!
Существует различие в семантике между двумя вызовами: вызов виртуальной подпрограммы диспетчеризуется во время выполнения в соответствии с типом указуемого объекта, в данном случае Derived_Class; вызов обычной подпрограммы связывается статически во время компиляции в соответствии с типом указателя, ъ данном случае Base_Class. Это различие весьма существенно, потому что изменение, которое состоит в замене невиртуальной подпрограммы на виртуальную подпрограмму или обратно, может вызвать ошибки во всем семействе классов, полученных из базового.
Динамическая диспетчеризация в языке C++ рассчитана на вызовы виртуальных подпрограмм, осуществляемые через указатель или ссылку.
Реализация
Ранее мы отмечали, что если подпрограмма не найдена в производном классе, то поиск делается в предшествующих классах, пока не будет найдено определение подпрограммы. В случае статического связывания поиск можно делать во время компиляции: компилятор просматривает базовый класс производного класса, затем его базовый класс, и так далее, пока не будет найдено соответствующее связывание подпрограммы. Затем для этой подпрограммы может компилироваться обычный вызов процедуры.
Если используются виртуальные подпрограммы, ситуация усложняется, потому что фактическая подпрограмма, которая должна быть вызвана, не известна до времени выполнения. Обратите внимание, что, если виртуальная подпрограмма вызывается с объектом конкретного типа, в противоположность ссылке или указателю, то все еще может использоваться статическое связывание. С другой стороны, решение, какую именно подпрограмму следует вызвать, основано на 1) имени подпрограммы и 2) классе объекта. Но первое известно во время компиляции, поэтому нам остается только смоделировать case-оператор по классам.
Обычно реализация выглядит немного иначе; для каждого класса с виртуальными подпрограммами поддерживается таблица диспетчеризации (см. рис. 14.5). Каждое значение класса должно «иметь при себе» свой индекс для входа в таблицу диспетчеризации для порождающего семейства, в котором оно определено. Элементы таблицы диспетчеризации являются указателями на таблицы переходов; в каждой таблице переходов содержатся адреса входов в виртуальные подпрограммы. Обратите внимание, что два элемента таблицы переходов могут указывать на одну и ту же процедуру; это произойдет, когда класс не замещает виртуальную подпрограмму. На рисунке cls3 произведен из
cls2, который в свою очередь произведен из базового класса cls1. Здесь cls2 заместил р2, но не р1, в то время как cls3 заместил обе подпрограммы.
Когда встречается вызов диспетчеризуемой подпрограммы ptr->p1(), выполняется код наподобие приведенного ниже, где мы подразумеваем, что неявный индекс — это первое поле указуемого объекта:
load RO.ptr Получить адрес объекта
load R1 ,(RO) Получить индекс указуемого объекта
load R2,&dispatch Получить адрес таблицы отправлений
add R2.R1 Вычислить адрес таблицы переходов
load R3,(R2) Получить адрес таблицы переходов
load R4,p1(R3) Получить адрес процедуры
call (R4) Вызвать процедуру, адрес которой находится в R4
Даже без последующей оптимизации затраты на время выполнения относительно малы, и, что более важно, фиксированы, поэтому в большинстве приложений нет необходимости воздерживаться от использования динамического полиморфизма. Но все же издержки существуют и применять динамический полиморфизм следует только после тщательного анализа. Лучше избегать обеих крайностей: и чрезмерного использования динамического полиморфизма только потому, что это «хорошая идея», и отказа от него, потому что это «неэффективно».
Обратите внимание, что фиксированные затраты получаются благодаря тому, что динамический полиморфизм ограничен фиксированным набором классов, порожденных из базового класса (поэтому может использоваться таблица диспетчеризации фиксированного размера), и фиксированным набором виртуальных функций, которые могут быть переопределены (поэтому размер каждой таблицы переходов также фиксирован). Значительным достижением языка C++ была демонстрация того, что динамический полиморфизм может быть реализован без неограниченного поиска во время выполнения.
14.5. Объектно-ориентированное программирование на языке Ada 95
В языке Ada 83 наличие пакетной конструкции обеспечивает полную поддержку инкапсуляции, а наличие производных типов частично обеспечивает наследование. Полного наследования нет, потому что, когда вы производите новый тип, то можете добавлять только новые операции, но не новые компоненты данных. Кроме того, единственный полиморфизм — это статический полиморфизм вариантных записей. В языке Ada 95 поддерживается полное наследование за счет того, что программисту дается возможность расширить запись производного типа. Чтобы обозначить, что родительский тип записи пригоден для наследования, его нужно объявить как теговый (tagged) тип записи:
package Airplane_Package is
type Airplane_Data is tagged
record
ID:String(1..80);
Speed: Integer range 0..1000;
Altitude: Integer range 0..100;
end record;
end Airplane_Package;
Этот тег аналогичен тегу в языке Pascal и дискриминанту в вариантных записях языка Ada, где он используется для того, чтобы различать разные типы, производные друг из друга. В отличие от этих конструкций, тег теговой записи неявный, и программист не должен явно к нему обращаться. Заглядывая вперед, скажем, что этот неявный тег будет использоваться, чтобы диспетчери-зовать вызовы подпрограмм для динамического полиморфизма.
Чтобы создать абстрактный тип данных, тип должен быть объявлен как приватный и полное объявление типа дано в закрытой части:
package Airplane_Package is
type Airplane_Data is tagged private;
procedure Set_ID(A: in out Airplane_Data; S: in String);
function Get_ID(A: Airplane_Data) return String;
procedure Set_Speed(A: in out Airplane_Data; I: in Integer);
function Get_Speed(A: Airplane_Data) return Integer;
procedure Set_Altitude(A: in out Airplane_Data; I: in Integer);
function Get_Altitude(A: Airplane_Data) return Integer;
private
type Airplane_Data is tagged
record
ID:String(1..80);
Speed: Integer range 0..1000;
Altitude: Integer range 0.. 100;
end record;
end Airplane_Package;
Подпрограммы, определенные внутри спецификации пакета, содержащей объявление тегового типа (наряду со стандартными операциями на типе), называются примитивными операциями, или операциями-примитивами (primitive operations) и являются подпрограммами, которые наследуются. Наследование выполняется за счет расширения (extending) тегового типа:
with Airplane_Package; use Airplane_Package;
package SST_Package is
type SST_Data is new Airplane_Data with
record
Mach: Float;
end record;
procedure Set_Speed(A: in out SST_Data; I: iri Integer);
function Get_Speed(A: SST_Data) return Integer;
end SST_Package;
Значения этого производного типа являются копиями значений родительского типа Airplane_Data вместе с (with) дополнительным полем записи Mach. Операции, определенные для этого типа, являются копиями элементарных подпрограмм; эти операции могут быть замещены. Конечно, для производного типа могут быть объявлены другие самостоятельные подпрограммы.
В языке Ada нет специального синтаксиса для вызова подпрограмм-примитивов:
A: Airplane_Data;
Set_Speed(A, 100);
С точки зрения синтаксиса объект А — это обычный параметр; И по его типу компилятор может решить, какую именно подпрограмму вызвать. Параметр называется управляющим, Потому что он управляет тем, какую подпрограмму выбрать. Управляющий параметр не обязан быть первым параметром, и их может быть несколько (при условии, что все они того же типа). Сравните это с языком C++, который использует специальный синтаксис, чтобы вы-звать подпрограмму, объявленную в классе:
C++ |
a.set_speed(100);
Объект а является отличимым получателем (distinguished receiver) сообщения set_speed. Отличимый получатель является неявным параметром, в данном случае обозначающим, что скорость (speed) будет установлена (set) для объекта а.