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

Дополнительные возможности, предоставляемые современными (и не очень современными) императивными языками программирования можно разделить на несколько групп:

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

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

Класс - это структура данных, объединенная с функциями обработки этих данных. На основе существующих (ранее определенных) классов могут быть созданы производные классы, в которых детализируются или заменяются свойства базовых классов. Возможность создания собственных классов - наверное, самое значительное усовершенствование традиционных языков программирования. Многие программы могут быть улучшены за счет использования классов, но привести короткий пример довольно сложно. Ниже приведен пример класса-массива, который может использоваться вместо шаблона функции сортировки. Более содержательный, но и гораздо более объемный пример библиотеки классов, предназначенных для реализации многооконного пользовательского интерфейса находится в архиве 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) // эффективность игнорируется { int I=0; while (I+1<N) { int M=I; int J=I+1; while (J<N) { if (Buff[M]>Buff[J]) { M=J; } J++; } T Temp =Buff[I]; Buff[I]=Buff[M]; Buff[M]=Temp; I++; } }

Каждый вызов функции Sort приведет к автоматическому созданию подходящей реализации функции Sort и вызову ее. Например, следующий код

int Buff1[1000]; double Buff2[1000]; Sort(1000, Buff1); Sort(1000, Buff2);

приведет к созданию двух функций Sort, предназначенных для сортировки массивов целых и вещественных чисел соответственно. Предполагается, что для каждого типа определен оператор ставнения ("больше") - для int и double он предопределен, для собственных классов его нужно определить.

Для работы приведенного примера существенна возможность перегрузки операторов, если ее нет он будет ограничен встроенными типами. Тем не менее перегрузка операторов не является необходимой, примерно то же может быть получено с помощью перегрузки функций:

template <class T> void Sort(int N, T *Buff) // эффективность игнорируется { int I=0; while (I+1<N) { int M=I; int J=I+1; while (J<N) { if (Comp(Buff[M],Buff[J])>0) { M=J; } J++; } T Temp =Buff[I]; Buff[I]=Buff[M]; Buff[M]=Temp; I++; } }

Чтобы использовать такую функцию для сортировки массива объектов некоторого типа должна быть определена функция Comp сравнивающая два объекта этого типа. Объекты должны передаваться по значению или по ссылке, но не по адресу (*).

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

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

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

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

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[8192]; 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

Создание экземпляров класса 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 строк текста. Не могу сказать, сколько все это заняло времени - известно только, что 24 июня 2001 года был написал разбор наследования (реализации методов классов не было), а 19 июля был работающий пример objects.ctx. В техническом плане ничего сложного здесь нет, сложнее решить, что должно быть и чего не должно быть в языке - эти-то вопросы и не решены до конца. Например, должны ли быть статические (не виртуальные) методы?

Компилятор context.120 является расширением context.110 и находится в архиве 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