Дополнительные возможности языков программирования
Дополнительные возможности, предоставляемые современными (и не очень
современными) языками программирования можно разделить на несколько групп:
Есть и другие, например 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
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+10 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
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 CArray
{
public:
T Buff[N];
...
};
void main()
{
CArray c;
CArray i;
...
}
Обработка исключений
Обработка исключений была реализована во многих языках. Это и C++, языки
некоторых СУБД (например, ORACLE или InterBase). Даже в Clipper'е
(распространенная в конце 80-х - начале 90-х xBase-подобная СУБД)
был подобный механизм. Пример на языке C++:
#include
#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 добавлено три ключевых слова:
- virtual - виртуальный метод
- base - ссылка на базовый класс
- self - ссылка на себя
Первое слово может использоваться при объявлении метода класса, два
других - внутри методов класса. Для объявления класса используется то же
ключевое сово, что и для объявления структуры (struct). Реализация примерно
(но не точно) соответствует Turbo Pascal. Приведенный здесь пример класса
CArray дает достаточное представление о синтаксисе. В отличие от Turbo
Pascal, заголовок реализации метода должен точно повторять его объявление
(слово virtual также должно быть повторено) - думаю, это улучшает читаемость
текста. Пока никак не решен вопрос о создании экземпляра класса в свободной
памяти, реализовано только автоматическое размещение экземплятов классов в
области данных или в стеке - для вызовов виртуальных методов никакой явной
инициализации экземпляра класса не требуется - инициализация выполняется
посредством неявного вызова конструктора. Поля классов также инициализируются
автоматически.
Вот перечень дополнений, которые нужно внести в компилятор:
- Дополнить структуру элемента таблицы полей
- Признаком типа поля (поле/метод/виртуальный метод)
- Ссылкой на элемент таблицы функций
- Дополнить разбор описания структуры
- разбором заголовков методов
- формированием конструктора класса
- Дополнить разбор функции
- Дополнить разбор выражений
- Еще одной областью видимости - полями класса
- Распознаванием base
- Обрабокой вызовов методов класса (в т.ч. косвенных)
- Изменить правила проверки допустимости присваивания
- Разрешить присваивание указателю на класс указателя на производный класс
- Запретить присваивание экземпляров полиморфных классов
- Формировать таблицы виртуальных методов классов
- Автоматически вызывать конструкторы экземпляров классов
Запрет присваивания экземпляров полиморфных классов - не очень красивое
решение, но оно предотвращает компиляцию следующей некорректной программы:
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