Типичные команды виртуальной машины для языка ООП
Команда
Пояснение
NOP
Ничего не делает
RETURN
Возврат из функции
PUT
Установка поля в объекте
GET
Доступ к полю в объекте
INVOKE
Вызов метода
CHECK
Проверка соответствия типа объекта
Формат команд АМ имеет вид: s e c d m→ s' e' c' d' m' – переход от старого состояния к новому.
Таб л иц а 3 7
Дополнительная спецификация команд виртуальной машины для языка ООП.
(t – логическое значение)
Исходной состояние
Результат s e (NOP . c) d m s e c d m s e (RETURN . c) d m s e c d m
(f v . s) e (PUT i . c) d m s e c d (m | m[f,i] = v)
(f . s) e (GET i . c) d m
(m[f,i] . s) e c d m
((a1 a2 ... aK) f .s) e (INVOKE sig . c) d m s ((a1 a2 ... aK) . e) (f[sig] . c) d m
(Obj . s) e (CHECK type . c) d m
(t . s) e c d m
Главный путь к снижению трудоѐмкости программирования связан с упрощением процесса отладки, который зависит от искусства
146
декомпозиции постановок задач и программ их решения на такие комплекты компонент, часть которых можно найти в библиотеках готовых модулей, а часть можно при программировании довести до уровня многократно используемых компонент.
Современное состояние имеющихся технических решений в данной области характеризуется доминированием компонентных технологий, ориентированных на ООП, обеспечивающих классификацию конструктива на уровне понятий пользователя и его отображение на уровень целевых архитектур, представимых в терминах абстрактных машин. При таком подходе не получают полного выражения функциональная декомпозиция и системные решения промежуточного уровня, что отчасти компенсируется развитием аспектно-ориентированного подхода, выглядящего как мета- надстройка над ООП [48]. Отдельный ряд трудностей вызывают приаппаратные оптимизации, требующие более тонкой детализации ниже традиционного уровня абстрактных машин.
Более реальна перспектива снижения трудоѐмкости и повышения надѐжности практического программирования повышением кратности использования библиотечных модулей в рамках многоязыкового программирования на базе систем программирования, создаваемых из общего, а потому более отлаженного конструктива.
7.3. С++
С концепцией ООП связано представление о возможности сокрытия информации, наследования определений по иерархии классов и полиморфизма реализации операций и функций. Переход к ООП в языке
С++ привел к пересмотру некоторых решений на уровне языка и компилятора. Рассмотрим особенности C++ как наиболее популярного языка ООП, в котором достижима схема, обобщающая комплекс решений задачи в виде ацикличного графа с возможными горизонтальными связями.
Такие решения направлены на программирование ряда версий решения задачи без отмены ранее отлаженных решений, но с формированием новых областей видимости, в которых устаревшая часть программы может быть просто оттеснена. Важно принять во внимание следующее:
– компилируемая программа на С++ представляет собой иерархию областей видимости определений элементов классов, доступ к которым регламентирован;
– компиляция методов, конструкторов, функций и перегруженных операций управляется форматом списка фактических параметров, что привело к более жѐстким правилам объявления типов данных и
147
ограничивает свободу конкретизации списка параметров при вызове функций;
– возникают рекомендательные средства повышать эффективность кодирования вызовов функций указанием на inline-включение;
– появляется уровень программирования шаблонов для представления общих схем обработки контейнерных типов разнотипных данных.
Программа
Пояснение
//HELLO.CPP
#include
Void main ()
{ cont << ”\nHello, World!\n” ;
}
Комментарий с именем файла программы
Препроцессор с вызовом библиотеки
Головная функция
Вывод приветствия на стандартное устройство
Пример 50. Программа на языке С++
Практический выигрыш от ООП можно показать на технике применения средств вывода данных.
Фрагмент
Пояснение printf (“x = %d, y = % s”, x, y);
Вывод целого и строки. «x» должен быть целым, а «y» – строкой
Пример 51. Вывод по библиотеке функций stdio.h
Программист должен знать, что первый параметр задает формат вывода и отследить согласование его с числом и типами выводимых данных и обозначениями форматов для функции printf.
Фрагмент
Пояснение cout << “x = ” << x << “, y = ” << y ; Поток вывода управляется фактическим типом данных.
Пример 52. Вывод по библиотеке классов iostream.h
Программисту достаточно перечислить элементы вывода в естественном порядке. Библиотека классов iostream.h содержит перегрузку операции «<<» для всех основных типов данных, что позволяет компилятору выбрать нужный шаблон кода программы, и программист освобожден от необходимости представлять в программе сведения о формате выводимых данных.
148
Учитывая
примеры описания и определения классов, можно сделать вывод, что внешние изменения в тексте программы на С++ в сравнении с текстом на C выглядят как появление ряда новых спецификаторов, с помощью которых как бы задается разметка программы, на области видимости, связанные с иерархией классов и дисциплиной доступа к элементам структурированных объектов класса. В результате вместо анализа последовательностей изменения состояний памяти по всей программе достаточно проанализировать изменения в пределах синтаксически выделенных путей по иерархии наследования методов объектов.
Доступность вложенного класса ограничивается областью видимости лексически объемлющего класса. Если у класса есть конструктор, он вызывается всякий раз при создании объекта этого класса. Если у класса есть деструктор, он вызывается всякий раз, когда уничтожается объект этого класса.
Чтобы можно было описать массив объектов класса с конструктором, этот класс должен иметь стандартный конструктор, вызываемый без параметров. В описании массива объектов не предусмотрено возможности указать параметры для конструктора. Когда уничтожается массив, деструктор должен вызываться для каждого элемента массива.
Производный класс наследует базовый класс, он больше своего базового класса в том смысле, что в нем содержится больше данных, и определено больше функций. Производный класс сам, в свою очередь, может быть базовым классом. Такое множество связанных между собой классов обычно называют иерархией классов. Обычно она представляется деревом, но бывают иерархии с более общей структурой в виде ориентированного графа. У класса может быть несколько прямых базовых классов. Возможность иметь более одного базового класса влечет за собой возможность неоднократного вхождения класса как базового.
С помощью виртуальных функций можно иметь
разные версии в разных производных классах, а выбор нужной версии при вызове – это задача транслятора. Тип функции указывается в базовом классе и не может быть переопределен в производном классе. Класс, в котором есть виртуальные функции, называется абстрактным. Абстрактный класс можно использовать только в качестве базового для другого класса.
149
Фрагмент
Пояснение
class complex { double re, im; public: complex(double r, double i) { re=r; im=i; }
friend complex operator+(complex,
complex); friend complex operator*(complex, complex);
};
Объявление класса объектов с закрытыми полями и общедоступными конструкторами и перегрузкой операций.
Пример 53. Перегрузка арифметических операций для их использования в выражениях над комплексными числами
Члены класса создаются в порядке их описания, а уничтожаются они в обратном порядке. Член класса может быть частным (private), защищенным
(protected) или общим (public).
Фрагмент
Пояснение
void f()
{ complex a = complex(1,3.1); complex b = complex(1.2,2);
complex c = b; a = b+c; b = b+c*a; c = a*b+complex(1,2);
}
Объявление функции, выполняющей процедуру, конструирования 3-х комплексных чисел и их инициирования, и обработка чисел с помощью перегруженных операций.
Пример 54. Интерпретация этих операций задана определениями функций с именами operator+
и operator*
Если b
и c
имеют тип complex, то b+c означает (по определению) operator+(b,c)
Сохраняются обычные приоритеты операций, поэтому второе выражение выполняется как b=b+(c*a)
, а не как b=(b+c)*a
. При перегрузке операций нельзя изменить их приоритеты, равно как и синтаксические правила для выражений.
Для операций преобразования ТД выбран подход, при котором проверка соответствия ТД является строго восходящим процессом, когда в
150
каждый момент рассматривается только одна операция с операндами, типы которых уже прошли проверку.
Вызов функции, т. е. конструкцию выражение(список выражений), можно рассматривать как бинарную операцию, в которой выражение является левым операндом, а список выражений – правым. Операцию вызова можно перегружать, как и другие операции.
Одним из самых полезных видов классов является контейнерный класс, т. е. такой класс, который хранит объекты каких-то других типов. Списки, массивы, ассоциативные массивы и множества – все это контейнерные классы.
Фрагмент
Пояснение
template class stack {
T* v;
T* p; int sz; public: stack(int s) { v = p = new T[sz=s]; }
stack() { delete[] v; } void push(T a) { *p++ = a; }
T pop() { return *--p; } int size() const { return p-v; }
};
Объявление параметризованного шаблона.
Для создания классов стеков в зависимости от задаваемого типа хранимых элементов при известном объѐме стека.
Общедоступны: конструкторы и деструкторы стека, функции работы со стеком
Пример 55. Шаблон типа для класса.Стек, содержащий элементы произвольного типа
Префикс template
указывает, что описывается шаблон типа с параметром
T
, обозначающим тип, и что это обозначение будет использоваться в последующем описании. После того, как идентификатор
T
указан в префиксе, его можно использовать как любое другое имя типа.
Область видимости
T
продолжается до конца описания, начавшегося префиксом template
Имя шаблонного класса, за которым следует тип, заключенный в угловые скобки <>, является именем класса (определяемым шаблоном типа), и его можно использовать как все имена класса.
151
Поскольку все функции-члены класса stack являются подстановками, то и в этом примере транслятор создает вызовы функций только для размещения в свободной памяти и освобождения.
Функции в шаблоне типа могут и не быть подстановками.
В программе может быть только одно определение функции-члена класса и только одно определение шаблона типа для функции-члена шаблонного класса. Если требуется определение функции-члена шаблонного класса для конкретного типа, то задача системы программирования найти шаблон типа для этой функции-члена и создать нужную версию функции. В общем случае система программирования может рассчитывать на указания от программиста, которые помогут найти нужный шаблон типа. Возможна передача операций как параметров функций.
Рассмотренные средства представления иерархии классов объектов с возможностью множественного наследования, использования программируемых и встроенных конструкторов и деструкторов объектов, создания массивов объектов класса с контролем доступа к элементам, перегрузки операций и задания виртуальных функций, а также, операций преобразования ТД и шаблонов типа для обработки контейнерных структур данных суммарно образуют достаточно богатый арсенал для поддержки процесса практичной разработки программ при решении расширяющейся задачи, совмещѐнного с процессом декомпозиции программы на многократно используемые компоненты разного уровня абстрагирования от конкретики решаемой задачи и специфики системных реализационных решений.
При организации наследования в отличие от обобщенных функций работает модельобмена сообщениями
: –
объекты обладают свойствами;
– посылают сообщения;
– наследуют свойства
и методы от предков.
При переходе от обычного стандартного программирования с ООП связывают радикальное изменение способа организации программ. Это изменение произошло под давлением роста мощности оборудования. ООП
взламывает традиционное программирование по многим направлениям.
Вместо создания отдельной программы, оперирующей массой данных, приходится разбираться с данными, которые сами обладают поведением, а программа сводится к простому взаимодействию данных новой категории – объектов.
152
7.4. Функциональные модели ООП Чтобы сравнить дистанцию ООП с ФП П. Грем (Paul Graham) в описании стандарта языка Common Lisp предлагает рассмотреть модель встроенного в Lisp объектно-ориентированного языка (ОО-язык), обеспечивающего основы ООП. Встраивание ОО-языка показывает характерное применение ФП – моделирование разных стилей программирования (начиная со стандартного программирования в виде prog-формы, предложенной Дж. Маккарти). В языке Lisp есть разные способы размещать коллекции свойств. Один из них – представлять объекты какхэш-таблицы и размещать свойства как входы в нее. П. Грем приводит пример (8 строк) реализации ООП на базе хэш-таблиц.
Фактически наследование обеспечивает единственная особенность языка
Lisp: все это работает благодаря реализации рекурсивной версии
GETHASH. Впрочем, Lisp по своей природе изначально был ОО-языком.
Определение методов может достичь предельной гибкости благодаря возможности генерировать определения функциональных объектов с помощью DEFMACRO или функцией категории FSUBR.
Реализация ООП с помощью хэш-таблиц обладает слегка парадоксальной окраской: гибкость у нее больше, чем надо и за большую цену, чем можно позволить. Уравновесить это может подобная реализация на базе простых векторов. Этот переход показывает, как функциональное программирование дает новое качество «на лету». В опорной реализации фактически не было реализационного разделения объектов на экземпляры и классы. Экземпляр – это просто класс с одним-единственным предком.
При переходе к векторной реализации разделение на классы и экземпляры становится реальным, также становится невозможным превращать экземпляры в классы простым изменением свойства.
Более прозрачная модель ООПна базе обычных списков, иллюстрирующая глубинное родство ФП и ООП, реализована в системе
CLOS. Показанный ниже пример 56 работает по первому аргументу (выбор подходящего метода рассчитан на то, что достаточно разобраться с одним аргументом),
CLOS
делает это на всех аргументах, причем с рядом вспомогательных средств, обеспечивающих гибкий перебор методов и анализ классов объектов
. Рассмотрим примеры использования
классов и экземпляров объектов:
(defclass ob () (f1 f2 ...))
153
Это означает, что каждое вхождение объекта будет иметь поля-слоты f1 f2 ...
(Слот – это поле записи или списка свойств.) Чтобы сделать представителя класса, мы вызываем общую функцию:
(SETF с (make-instance 'ob))
Чтобы задать значение поля, используем специальную функцию:
(SETF (slot-value c) 1223)
До этого значения полей были не определены.
Простейшее определение слота – это его имя. Но в общем случае слот может содержать список свойств.
Внешне свойства слота специфицируются как ключевые параметры функции. Это позволяет задавать начальные значения. Можно объявить слот совместно используемым:
:allocation :class
Изменение такого слота будет доступно всем экземплярам объектов класса. Можно задать тип элементов, заполняющих слот, и сопроводить их строками, выполняющими роль документации. Нет необходимости все новые слоты создавать в каждом классе, поскольку можно наследовать их из суперклассов.
Определение
Пояснение
(defclass expr ()
((type :accessor td)
(sd :accessor ft))
(:documentation "C-expression"))
Суперкласс для всех структур Lisp-выражений.
Заданы ключи доступа к общим полям объектов: тип и операнд.
(defclass un (expr)
((type :accessor td)
(sd :accessor ft) )
(:documentation "quote car *other *adr"))
Класс для унарных форм.
Доступ можно унаследовать от суперкласса, а здесь не дублировать.
(defclass bin (expr)
((type :accessor td)
(sd :accessor ft)
(sdd :accessor sd) )
(:documentation "cons + lambda let"))
Класс бинарных форм.
Третье поле для второго операнда.
154
(defclass trio (expr)
((type :accessor td)
(sd :accessor ft)
(sdd :accessor sd)
(sddd :accessor td) )
(:documentation "if label"))
Если взять суперкласс (bin), то можно не объявлять первые 3 поля.
Четвѐртое поля для третьего операнда.
(defmethod texrp ((x expr) (nt atom))
(SETF (slot-value x 'type) nt)
(SETF (td x) nt) ;;--;; variant
(:documentation "
объявляем тип выражения"))
Метод представления выражений для компиляции.
(defmethod spread ((hd (eql 'QUOTE))
(tl expr))
(let ( (x (make-instance 'un)) )
(SETF (ft x) (car tl))
(SETF (td x) hd)
) (:documentation "
распаковка выражения"))
Метод разбора констант и выражений с унарными операциями.
(defmethod compl ((hd (eql 'QUOTE))
(tl expr))
(list 'LDC tl)
(:documentation "
сборка кода"))
Метод компиляции констант.
(defmethod compl ((hd (eql 'CAR))
(tl expr) N)
(append (compl(ft tl) N) '(CAR))
(:documentation "
сборка кода"))
Метод компиляции выражений с унарными операциями.
(defmethod spread ((hd (eql 'CONS))
(tl expr))
(let ( (x (make-instance 'bin)) )
(SETF (ft x) ( CAR tl))
(SETF (sd x) ( cadr tl))
(SETF (td x) hd)
) (:documentation "
распаковка выражения"))
Метод разбора выражений с бинарными операциями.
(defmethod compl ((hd (eql 'CONS))
(tl bin) N )
(append (compl(sd tl) N) (compl(ft tl) N)
'(CONS) )
(:documentation "
сборка кода"))
Метод компиляции выражений с бинарными операциями с прямым порядком компиляции операндов.
(defmethod compl ((hd (eql '+))
(tl bin) N )
(append (compl(ft tl) N) (compl(sd tl) N)
'(ADD) )
(:documentation "
сборка кода"))
Метод компиляции выражений с бинарными операциями с обратным порядком компиляции операндов
(defmethod spread ((hd (eql 'IF))
(tl expr) )
(let ( (x (make-instance 'trio)) )
Метод разбора выражений с триадными операциями.
155
(SETF (ft x) ( CAR tl))
(SETF (sd x) ( cadr tl))
(SETF (td x) ( caddr tl))
(SETF (td x) hd)
) (:documentation "
распаковка выражения"))
(defmethod compl ((hd (eql 'IF))
(tl expr) N )
(let ( (then (list (compl(sd tl)N) '(JOIN)))
(else (list (compl(td tl)N) '(JOIN))) )
(append (compl(ft tl)N) (list 'SEL then else)
)
)(:documentation "
сборка кода"))
Метод компиляции выражений с триадными операциями.
(defmethod parh ((x expt))
(let (ftx (ft x))
(COND
((ATOM ftx) (spread 'ADR ftx))
((member (CAR ftx)
'(QUOTE CAR CONS + IF LAMBDA LABEL
LET))
(spread (CAR ftx) (CDR ftx))
(T (spread 'OTHER ftx) ))
)(:documentation "
шаг разбора"))
Метод разбора произвольных выражений.
Пример 56. ОО-определение Lisp-компилятора
CLOS
, естественно, использует модель обобщенных функций, но мы написали независимую модель, используя более старые представления, тем самым показав, что концептуально ООП – это не более чем перефразировка идей Lisp-а. ООП – это одна из вещей, которую Lisp изначально умеет делать. Для функционального стиля программирования в переходе к ООП нет ничего революционного. Такой переход практически не расширяет пространство решений. Это просто небольшая конкретизация механизмов перебора ветвей функциональных объектов. При переходе от императивного стиля ООП компенсирует избыточную целостность представления отлаживаемых программ, смягчает жесткость зависимости компонентов программы от потока информационных процессов.
Более интересный вопрос, что же еще может дать функциональный стиль и традиция реализации функциональных систем программирования?
– Похоже, что это средства освоения новых архитектур и технологий.
Другая модель ООП, полученная на базе обычных списков свойств
(атрибутов), также иллюстрирует глубинное родство ФП и ООП [10].
Нужно лишь уточнить определение Lisp-интерпретатора, чтобы методы
156
рассматривались как особая категория функций, обрабатываемая специальным образом.
7.5. Спецификация Таб л иц а 3 8