Лекция 19 (1160817), страница 2
Текст из файла (страница 2)
px
f X::f
g X::g
Пусть есть класс Y, выведенный из Х, в котором мы переопределяем функцию f, и класс Z, выведенный из Y, в котором мы переопределяем функцию g. Значит Y наследует g от Х, а класс Z наследует f от Y. В классе Y также определяется виртуальная функция h, которая переопределяется в Z. ТВМ для Y
f Y::f
g X::g
h Y::h
Т.к. класс Y наследуется от Х, то начало ТВМ для Y будет таким же как и для Х, только f переопределяется. ТВМ для класса Z
f Y::f
g Z::g
h Z::h
Вызов. У нас есть указатель
px->f( );
В каждом объекте класса Х имеется указатель на таблицу виртуальных имен. Считая от начала класса ссылка на ТВМ всегда имеет фиксированное смещение для всех объектов. Поэтому компилятор знает, что под фиксированным смещением стоит ссылка на ТВМ, и какой вид имеет ТВМ для класса Х: по нулевому смещению в ней стоит адрес f. Задача компилятора косвенно добраться до таблицы виртуальных имен, взять из нулевого смещения адрес f и косвенным образом вызвать f. Понятно, откуда берутся накладные расходы: мы должны по этому указателю получить адрес соответствующей функции и сделать косвенный вызов. Что особенно приятно, эти накладные расходы фиксированы для любого типа данных, в отличии от языка Smalltalk. Если во всех языках, которые мы рассматриваем, мы объявляем динамику связывания метода при трансляции, равно как и его сигнатуру, то в языке Smalltalk таблица методов может пополняться динамически. Память в языке Smalltalk распределяется не линейным образом, а цепным. Если у нас есть некоторая иерархия
O bject
Х
Y
Z
Если ссылка указывает на объект класса Z и есть вызов какого-то объекта, то поиск метода производится следующим образом: динамически в таблице проверяется существует реализация такого метода или нет. Если не существует мы идем на суперкласс Y, если нет в таблице методов для этого класса, то идем на класс Х и т.д. до тех пор, пока мы не дойдем до типа данных Object, который является вершиной соответствующей иерархии. Если и в Object нет реализации этого метода, тогда динамически выдается сообщение об ошибке. Т.е. компилятор языка Smalltalk в принципе не может проверить есть такой метод или нет, поскольку речь идет о чистой динамике. Чем длинней иерархия классов, тем больше может занять поиск соответствующего метода. С практической точки зрения подобного рода реализация является очень гибкой. Но для реальных промышленных приложений, где требуется эффективность, такой метод реализации не является приемлемым. В С++ и всех языках, которые на нем основаны, есть небольшие накладные расходы (порядка 6 – 9 ассемблерных команд), но они во многих случаях являются вполне приемлемыми. Они никак не зависят от глубины иерархии.
ТВМ создается одна для всех экземпляров класса. Вроде бы это не большие расходы по памяти – заводить для каждого типа данных свою ТВМ, но в некоторых случаях такой расход является слишком накладным. Рассмотрим типичный пример: система графического интерфейса пользователя. Типичная для этого случая иерархия классов
CWindow
CMenu CFrameWind CDlgWindow CViewWindow
CFrameWind - главное окно. Типичное приложение – это Frame Window, внутри которого всавлено View Window и иногда может возникать Dialog Window и т.д. – по одному экземпляру каждого класса. Все нормальные системы интерфейса пользователя являются событийно-ориентированными. Для каждого вида событий должна быть задана в базовом классе функция – обработчик этого события. В базовой версии Windows было 144 события. Если реализовывать такой механизм обработки событий с классической объектно-ориентированной точки зрения, то каждая таблица виртуальных методов должна иметь по меньшей мере 144 метода (реакции на данные события). 144 метода для каждого типа окон и для базового типа. Причем у нас всего по одному экземпляру каждого, и с каждым экземпляром, в результате, у нас связана своя ТВМ. Т.е. накладные расходы по памяти на ТВМ становятся больше, чем на объекты самого класса. Это пример не реальной иерархии. Реальные иерархии классов, например, в MFC, значительно длиннее, и как правило заводится по одному - два объекта для каждого класса такой иерархии. Накладные расходы на ТВМ становятся слишком большими. Поэтому, как правило, событийно-ориентированные интерфейсы с огромным количеством событий, где каждый объект в иерархии должен реагировать на каждое возможное событие, реализуются с помощью механизма динамической диспетчеризации, когда заводится некоторая табличка (она как правило является хеш-таблицей), в которую включаются только те указатели, которые реально есть. Если CWindow умеет отвечать только на 10 из этих событий, а остальные просто игнорирует, то в соответствующей таблице будет 10 указателей. В свое время фирма Borland стала расширять реализацию языка С++, чтобы допускать подобного рода вещи. И в языке Delphi, и в языке Borland C++ появились динамические методы, которые были реализованы через динамическую диспетчеризацию. А фирма Microsoft пошла по другому пути: она стала создавать так называемые таблицы сообщений с помощью макросов, оставаясь в рамках стандартной реализации языка С++. Достаточно много менеджеров программных проектов не захотели принимать идеологию Borland потому, что Borland специфическим образом расширял язык С++.
Оберон.
В первой версии языка Оберон, которая вышла в 1988 г. динамического связывания не было в принципе. Через год или два появилась реализация языка Оберон-2, в котором динамическое связывание уже было. (Оберон-2 – расширение языка Оберон динамическим связыванием.) Оберон-2 вводит понятие процедур динамически привязанных к типу.
TYPE T = RECORD
…
END
Обычная процедура выглядит так
PROCEDURE P(VAR X:T);
Никакой динамической привязки здесь нет.
PROCEDURE (VAR X:T) P( );
Это означает, что процедуру Р можно переопределять, если ее динамическим параметром будет производный от Т класс.
TYPE T1 = RECORD(T)
…
END
Для класса Т1 мы имеем право написать свою реализацию процедуры Р
PROCEDURE (VAR X:T1) P( );
В языке Оберон любое перекрытие и замещение имен запрещено. Но для динамически связанных процедур и функций это допустимо. В зависимости от того, какой будет динамический тип соответствующего аргумента, будет вызываться нужная процедура Р.
PROCEDURE F(VAR X: T);
В процедуре F есть вызов
X.P( );
Вызов очень похож на вызов методов из ООП.
VAR A: T;
B:T1;
Компилятор, когда транслирует функцию F, не знает какого реально динамического типа будет Х (Х передается по ссылке). При вызове F(A) будет вызываться Р для типа Т, при вызове F(B) будет вызываться Р для типа Т1 => это зависит от того каким будет фактический параметр. Не зная фактического параметра, оттранслировать вызов не можем, компилятор вставляет соответствующий динамический вызов. Даже по синтаксису они очень напоминают виртуальные методы в языках С++, C#, Java, Delphi.
Динамическое связывание методов в языке Ада 95.
Необходимо было, чтобы новая система типов, т.е. объектно-ориентированное расширение языка, включалась бы в язык мягким образом. Т.е. программы на языке Ада 83 одновременно должны быть программами на языке Ада 95. Это достаточно сложная задача, и создатели Ада 95 решили ее достаточно адекватно.
tagged record …end
Такие записи можно расширять.
type T1 is new T with record … end;
Т обязан быть помеченной записью или наследником помеченной записи. Без динамического связывания с тем же успехом можно было бы говорить об отношении включения. В языке Ада введено понятие class-wide types (cw-типы или типы класса). Если Т – расширяемый класс (т.е. либо это помеченная запись, либо запись, унаследованная от помеченной), то тогда вводится понятие
T'class
class – новое ключевое слово (программы на языке Ада 83, которые использовали одноименные идентификаторы стали непроходимыми, нужно один раз перекомпилировать). Это чем-то похоже на понятие неограниченных типов в Аде (например, массив, у которого не фиксировался диапазон). Переменные таких типов имели смысл только как формальные параметры процедур. Объектами этого типа могут являться переменные типа Т + переменные всех типов, которые могут быть выведены из Т.
X:T'class;
Х – переменная, которой можно присваивать любой объект типа Т + любой объект типа производного от Т. С точки зрения реализации Х выступает как ссылка на объект типа Т. Если у нас есть какая-то иерархия
Т => T1 => T2
A: T; B: T1; C: T2;
Можно производить присваивания
X:= A; X:= B; X:= C;
Ссылки могут иметь динамический тип. Представим, что у нас есть пакет М, в котором определяется тип данных Т и
procedure P(X: T);
Пусть в пакете М1 из типа Т выводится тип Т1 и переопределяется процедура
procedure P(X: T1);
(это можно было делать и в Ада 83). Пусть в пакете М2 из типа данных Т1 выводится тип данных Т2 и переопределяется процедура
procedure P(X: T2);
В других языках (в Обероне, в C#, в Delphi) специально помечаются методы, которые могут быть динамическими. Рассмотрим
PROCEDURE F1(X: in out T);
PROCEDURE F2(X: T'class);
В процедурах F1 и F2 вызывается процедура
P(X);
Это все вполне допустимо. В случае вызова P(X) в процедуре F1 никакой динамической привязки. Для всех случаев
F1(A); F1(B); F1(C);
поскольку речь идет о типе данных Т, то будет вызываться процедура Р для Т (т.е. Р из модуля М) потому, что компилятор в этом месте просто вставит статическую привязку. Для F2 ситуация другая.
F2(A); F2(B); F2(C);
Здесь будет вызываться Р(Х) в зависимости от динамического типа. В случае F2(А) будет вызвана процедура Р для типа Т, в случае F2(B) – процедура Р для типа Т1, в случае F2(C) – процедура Р для типа Т2. Вызов Р(х) в F2 будет динамически привязан. Т.е. одну и ту же процедуру можно вызывать как статически привязанной, так и динамически привязанной – все зависит от параметра.
Это выглядит очень похоже. На самом деле такой механизм привязки оказывается несколько богаче, чем может показаться на первый взгляд. Для всех методов, которые мы писали для С++ и прочих языков, аналогичные вещи можно сделать и для языка Ада. Но язык Ада (Ада 95) позволяет еще некоторое расширение, которое не позволяют все те языки, о которых мы говорили. Это так называемые мультиметоды. Фактически мультиметоды реализованы как понятие только в языке Ада 95. В С++ Страуструп в своей книге обсуждал потребность в мультиметодах, но тем не менее мультиметоды в языке С++ не реализованы. Слово мультиметоды не имеет никакого отношения к множественному наследованию. Мультиметод – это динамически связанный метод, который вызывается в зависимости от динамического типа нескольких объектов. До этого динамическая привязка всегда шла по одному типу объекта. В классовых языках метод класса вызывается в зависимости от динамического типа соответствующего объекта х