Дополнительные возможности языков программирования

Дополнительные возможности, предоставляемые современными (и не очень современными) языками программирования можно разделить на несколько групп: Есть и другие, например RTTI (Run-Time Type Information) или пространства имен, но думаю, их значение не столь велико. В одних языках (C++) все они реализованы, в других присутствуют лишь некоторые. По-видимому, наибольшее значение имеет возможность создания новых классов, и она проще чем другие встраивается в компилятор. А может и нет - то что уже сделано, обычно не кажется сложным. Что же касается остальных расширений, то они здесь есть только упомянуты...

Классы, наследование и полиморфизм

Класс - это структура данных, объединенная с функциями обработки этих данных. На основе существующих (ранее определенных) классов могут быть созданы производные классы, в которых детализируются или заменяются свойства базовых классов. Возможность создания собственных классов - наверное, самое значительное усовершенствование традиционных языков программирования. Многие программы могут быть улучшены за счет использования классов, но найти короткий пример довольно сложно. Здесь приведен пример класса-массива, который может использоваться вместо шаблона функции сортировки. Более содержательный, но и гораздо более объемный пример библиотеки классов, предназначенных для реализации многооконного пользовательского интерфейса находится в архиве samples.zip (файл objects.ctx). Ниже приведено описание (к сожалению, очень краткое) классов этого примера и перечень необходимых доработок компилятора.

Перегрузка функций и операторов

Перегрузка функций позволяет создать одноименные функции, предназначенные для обработки аргументов разных типов. Например, для вывода на консоль данных разных типов можно определить несколько функций Print (C++): void Print(char *S); void Print(int I); void Print(double D); void main() { char *S="Hello, world!"; int I= 1; double D= 1.0; long L= 1; Print (S); Print (I); Print (D); Print (L); // Ошибка - более одной подходящей функции } Встретив имя Print, компилятор должен выбрать из имеющихся функций Print наиболее подходящую (с наиболее подходящими параметрами). Для самого компилятора это означает, что Таким образом, компиляция вызова функции становится похожей на компиляцию оператора, где нужный код выбирается в зависимости от типов операндов. Можно говорить о большем единообразии языка, но и о большей сложности компилятора (компиляция вызова не может быть выполнена за один проход, т.к. заранее неизвестно, как должны быть преобразованы аргументы).

Перегрузка операторов дает возможность создания новых типов, использование которых сходно с использованием встоенных типов. Например, можно создать тип complex (комплексное число), который является структурой, но переменные этого типа могут использоваться в выражениях так же, как переменные типа double:

struct complex { double re; double im; complex() {re=0.0; im=0.0;}; complex(double r, double i) {re=r; im=i; }; }; complex operator +(complex &x, complex &y) { return complex(x.re+y.re, x.im+y.im); } complex operator -(complex &x, complex &y) { return complex(x.re-y.re, x.im-y.im); } complex operator *(complex &x, complex &y) { return complex(x.re*y.re-x.im*y.im, x.re*y.im+x.im*y.re); } complex operator /(complex &x, complex &y) { double r(y.re*y.re+y.im*y.im); return complex((x.re*y.re+x.im*y.im)/r,(x.im*y.re-x.re*y.im)/r); } Далее можно написать complex x(1,2); complex y(2,3); complex z=x+y;

Шаблоны функций и классов

Шаблон - это описание, на основе которого компилятор сам создаст подходящую реализацию функции или класса. Простейший (но содержательный) пример - шаблон функции сортировки: template <class T> void Sort(int N, T *Buff); Каждый вызов функции Sort приведет к автоматическому созданию подходящей реализации функции Sort и вызову ее. Например, следующий код int Buff1[1000]; double Buff2[1000]; Sort(1000, Buff1); Sort(1000, Buff2); приведет к созданию двух функций Sort, предназначенных для сортировки массивов целых и вещественных чисел соответственно. Предполагается, что для каждого типа определен оператор сравнения - для int и double он предопределен, для собственных классов его нужно определить - вот связь с перегрузкой операторов.

Шаблонная функция гораздо лучше библиотечной функции языка C qsort:

void qsort(void *base, size_t num, size_t width, int (*cmp)(const void *elem1, const void *elem2));

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

Нечто похожее можно получить с помощью классов:

struct CArray int N; int Comp(int I, J) virtual; // функция сравнения void Swap(int I, J) virtual; // функция перестановки void Sort(); // функция сортировки end int CArray.Comp(int I, J) virtual end void CArray.Swap(int I, J) virtual end void CArray.Sort() // эффективность игнорируется int I=0; while I+1<N do int M=I; int J=I+1; while J<N do if Comp(M,J)>0 then M=J; end inc J; end Swap(I,M); inc I; end end struct CIntArray (CArray) int Buff[16384]; int Comp(int I, J) virtual; void Swap(int I, J) virtual; end int CIntArray.Comp(int I, J) virtual if Buff[I]<Buff[J] then return -1; end if Buff[I]>Buff[J] then return 1; end return 0; end void CIntArray.Swap(int I, J) virtual int Temp; Temp =Buff[I]; Buff[I]=Buff[J]; Buff[J]=Temp; end

Здесь использован синтаксис языка Context. Создание экземпляров класса CArray не имеет смысла, но на его основе могут быть созданы классы-массивы определенных типов и для этих массивов будет определена (унаследована) операция сортировки. Такая конструкция обеспечивает строгий контроль типов, но она менее эффективна, чем шаблон функции сортировки (т.к. всегда для сравнения элементов массива выполняется косвенный вызов функции сравнения).

Также могут использоваться шаблоны классов - описания, на основе которых компилятор создаст классы, например:

template <class T, int N> class CArray { public: T Buff[N]; ... }; void main() { CArray <char, 8192> c; CArray <int, 8192> i; ... }

Обработка исключений

Обработка исключений была реализована во многих языках. Это и C++, языки некоторых СУБД (например, ORACLE или InterBase). Даже в Clipper'е (распространенная в конце 80-х - начале 90-х xBase-подобная СУБД) был подобный механизм. Пример на языке C++: #include <stdio.h> #define is { #define then { #define begin try { #define raise throw #define when } catch #define do { #define end } class CException { }; class CException1 : public CException { }; class CException2 : public CException { }; void F(int N) is if (N>10) then CException2 E; raise E; end end void main() is begin F(10); F(20); F(30); // не выполняется when (CException1) do puts("CException1\n"); when (CException) do puts("CException\n"); end end

Механизм обработки исключений очень удобен. В частности, он позволяет легко прервать выполнение рекурсивной функции и вернуть управление функции, вызвавшей ее (например, для обработки ошибок в интерпретаторе или компиляторе). Но этот механизм может быть и источником проблем. Например, если в некоторой функции выполняется захват некоторых ресурсов (распределение памяти, открытие файлов и т.п.), инициирование исключения может привести к тому, что ресурсы не будут освобождены (область памяти нельзя будет освободить, т.к. блоку обработки исключения не будет доступен локальный указатель на нее). Одно из решений состоит в помещении всех операций захвата ресурсов внутрь классов, снабженных специальными методами уничтожения экземпляров классов (деструкторов, в них должно выполняться освобождение захваченных ресурсов) и автоматический вызов деструкторов в процессе выхода из программного блока (это уже делает компилятор).

Реализация объектно-ориентированного расширения

При разработке компилятора context 1.0 вопрос о реализациия классов не рассматривался. Я понимал, что это было бы желательно, но ничего не сделал. В данном случае никаких проблем не возникло - реализация классов естественным образом встраивается в язык и компилятор, но часто все бывает иначе - ограничения, заложенные при проектировании системы очень сложно преодолеть.

В описание языка Context добавлено три ключевых слова:

Первое слово может использоваться при объявлении метода класса, два других - внутри методов класса. Для объявления класса используется то же ключевое сово, что и для объявления структуры (struct). Реализация примерно (но не точно) соответствует Turbo Pascal. Приведенный здесь пример класса CArray дает достаточное представление о синтаксисе. В отличие от Turbo Pascal, заголовок реализации метода должен точно повторять его объявление (слово virtual также должно быть повторено) - думаю, это улучшает читаемость текста. Пока никак не решен вопрос о создании экземпляра класса в свободной памяти, реализовано только автоматическое размещение экземплятов классов в области данных или в стеке - для вызовов виртуальных методов никакой явной инициализации экземпляра класса не требуется - инициализация выполняется посредством неявного вызова конструктора. Поля классов также инициализируются автоматически.

Вот перечень дополнений, которые нужно внести в компилятор:

Запрет присваивания экземпляров полиморфных классов - не очень красивое решение, но оно предотвращает компиляцию следующей некорректной программы:

struct A int a; int f() virtual; end struct B (A) int b; int f() virtual; end begin A a; B b; A @p; @p=@b; p= a; // ошибка @p=@a; p= b; // также ошибка end Ошибка в том, что экземпляру класса B присваивается значение базового класса A, которое не достаточно для заполнения всех полей класса B. Второе присваивание (p=b) допустимо, но компилятор не в состоянии этого понять, т.к. не знает, на какой класс ссылается @p.

Реализация всего перечисленного уложилась в 1000 строк текста. А весь компилятор содержит чуть больше 6000 строк текста - на фоне Pascal-S не так уж и мало - одна из версий Pascal-S содержит всего 900 строк, правда после форматирования (одна строка - один оператор) число строк удваивается. В техническом плане ничего сложного нет, сложнее решить, что должно быть и чего не должно быть в языке - эти-то вопросы и не решены до конца. Не могу сказать, сколько все это заняло времени - известно только, что 24 июня 2001 года я написал разбор наследования (реализации методов классов не было), а 19 июля был работающий пример objects.ctx. А сохранившийся текст первого компилятора context.pas датирован 19 сентября 1994 года...

Компилятор context.sim является расширением context.opt и находится в архиве samples.zip. Для проверки компилятора использовался достаточно большой пример objects.ctx. Это упрощенный перевод написанной в 1992 году на Turbo Pascal библиотеки классов многооконного пользовательского интерфейса (примерный аналог Turbo Vision). Тогда это было совсем не просто - что-то осмысленное (но все равно достаточно убогое) получилось спустя месяц и раза с третьего. Проблема перекрытия окон была решена очень просто - при перерисовке одного из окон также выполнялась перерисовка всех вышележащих окон - при работе в текстовом режиме это допустимо. Несколько позже я написал аналогичную библиотеку для графического режима и статейку о ней в "Компьютер Пресс" №7 за 1993 год - совершенно неудачную. А еще чуть позже все было заброшено.

В отличие от прототипа в objects.ctx меньше оптимизации, объем вывода на экран заметно больше необходимого, поэтому на старых машинах пример не будет хорошо работать (прототип нормально работал на PC/XT, для чего потребовалось аккуратно устанавливать области отсечения и написать около двадцати команд на ассемблере - всего навсего). Более того, графическая версия библиотеки приемлемо работала на ПЭВМ "Нейрон" (85% от 4.77МГц IBM PC, монохромная графика 640*200), но в ней и ассемблера было больше и, что важнее, использовался совсем иной алгоритм вывода на экран - с помощью рекурсивной процедуры окно, в которое производится вывод разбивалось на множество полностью открытых прямоугольных областей и каждая из них копировалась из буфера в видеопамять. Задача казалась сложной, а ее решение ее было простым и изящным... Но это отступление, тестовый пример содержит следующие классы (стрелками показаны отношения наследования):

CScreen
Экран - образ видеопамяти и набор функций вывода. Экземпляр этого класса используется в качестве бувера вывода. Пока вывод не завершен, никаких изменеий на экране не происходит, после завершения вывода содержимое буфера просто копируется в видеопамять.
CMouse
Мышь - простой класс, реализующий функции опроса манипулятора "мышь".
CMessage
Сообщение - структура данных, в поля которой записывается информация о событии, команда и т.п.
CWindow
Окно - каждый отображаемый объект представляется классом, производным от CWindow. Каждое окно способно отобразить себя на экране (с помощью метода Show) и реагировать на происходящие события (с помощью метода Post). С помощью метода Send окно может послать сообщение системе.
CGroup
Группа - специальное окно, состоящее из других (вставленных в него) окон. Изображение группы формируется из изображений вложенных в нее окон, реакция на события в большинстве случаев перепоручается вложенным окнам.
CDialog
Окно диалога - это уже настоящее "окно" с рамкой, можно менять его размеры и положение с помощью мыши. Окно диалога включает в себя экземпляры вспомогательных классов CBox и CBackground.
CProgram
Программа - базовый класс для всех прикладных программ. Обеспечивает опрос клавиатуры и мыши, передачу команд и формирование изображения на экране. В методе Exec реализован цикл обработки событий.

Четыре класса в правой колонке (CChip-фишка, CPuzzle-головоломка, CClock-часы и CApp-приложение) являются объектами прикладной программы, Пример содержит три окна диалога, с помощью мыши можно перемещать их по экрану и менять их размеры.

Сайт создан в системе uCoz