Дополнительные возможности, предоставляемые современными (и не очень современными) императивными языками программирования можно разделить на несколько групп:
Классы, наследование и полиморфизм
Класс - это структура данных, объединенная с функциями обработки этих данных. На основе существующих (ранее определенных) классов могут быть созданы производные классы, в которых детализируются или заменяются свойства базовых классов. Возможность создания собственных классов - наверное, самое значительное усовершенствование традиционных языков программирования. Многие программы могут быть улучшены за счет использования классов, но привести короткий пример довольно сложно. Ниже приведен пример класса-массива, который может использоваться вместо шаблона функции сортировки. Более содержательный, но и гораздо более объемный пример библиотеки классов, предназначенных для реализации многооконного пользовательского интерфейса находится в архиве samples.zip (файл objects.ctx). В конце страницы приведено краткое описание классов этого примера и перечень необходимых доработок компилятора.
Перегрузка функций и операторов
Перегрузка функций позволяет создать одноименные функции, предназначенные для обработки аргументов разных типов. Например, для вывода на консоль различных данных можно определить несколько функций Print (C++):
Встретив имя Print, компилятор должен выбрать из имеющихся функций Print наиболее подходящую (с наиболее подходящими параметрами). Таким образом, компиляция вызова функции становится похожей на компиляцию оператора, где нужный код выбирается в зависимости от типов операндов. Можно говорить о большем единообразии языка, но и о большей сложности компилятора (компиляция вызова не может быть выполнена за один проход, т.к. заранее неизвестно, как должны быть преобразованы аргументы).
Перегрузка операторов дает возможность создания новых типов, использование которых сходно с использованием встоенных. Например, можно создать тип complex (комплексное число), который является структурой, но переменные этого типа могут использоваться в выражениях так же, как переменные типа double:
Далее можно написать
Шаблон - это описание, на основе которого компилятор сам создаст подходящую реализацию функции или класса. Простейший (но содержательный) пример - шаблон функции сортировки:
Каждый вызов функции Sort приведет к автоматическому созданию подходящей реализации функции Sort и вызову ее. Например, следующий код
приведет к созданию двух функций Sort, предназначенных для сортировки массивов целых и вещественных чисел соответственно. Предполагается, что для каждого типа определен оператор ставнения ("больше") - для int и double он предопределен, для собственных классов его нужно определить.
Для работы приведенного примера существенна возможность перегрузки операторов, если ее нет он будет ограничен встроенными типами. Тем не менее перегрузка операторов не является необходимой, примерно то же может быть получено с помощью перегрузки функций:
Чтобы использовать такую функцию для сортировки массива объектов некоторого типа должна быть определена функция Comp сравнивающая два объекта этого типа. Объекты должны передаваться по значению или по ссылке, но не по адресу (*).
Шаблонная функция гораздо лучше библиотечной функции языка C qsort:
void qsort(void *base, size_t num, size_t width, int (*cmp)(const void *elem1, const void *elem2));
т.к. обеспечивает контроль типов и [несколько] меньшее время выполнения (для сравнения элементов массива никогда не требуеся косвенный вызов функции, в некоторых случаях вызов функции вообще не требуется). Да и читается гораздо лучше.
Нечто похожее можно получить с помощью классов (Context):
Создание экземпляров класса CArray не имеет смысла, но на его основе могут быть созданы классы-массивы определенных типов и для этих массивов будет определена (унаследована) операция сортировки. Такая конструкция обеспечивает строгий контроль типов, но она менее эффективна, чем шаблон функции сортировки (т.к. всегда для сравнения элементов массива выполняется косвенный вызов функции сравнения).
Также могут использоваться шаблоны классов - описания, на основе которых компилятор создаст классы, например:
Обработка исключений была реализована во многих языках. Это и C++, языки некоторых СУБД (например, ORACLE или InterBase). Даже в Clipper'е (распространенная в конце 80-х - начале 90-х xBase-подобная СУБД) был подобный механизм. Пример на языке C++:
Механизм обработки исключений очень удобен. В частности, он позволяет легко прервать выполнение рекурсивной функции и вернуть управление функции, вызвавшей ее (например, для обработки ошибок в интерпретаторе или компиляторе). Но этот механизм может быть и источником проблем. Например, если в некоторой функции выполняется захват некоторых ресурсов (распределение памяти, открытие файлов и т.п.), инициирование исключения может привести к тому, что ресурсы не будут освобождены (область памяти нельзя будет освободить, т.к. блоку обработки исключения не будет доступен локальный указатель на нее). Одно из решений состоит в помещении всех операций захвата ресурсов внутрь классов, снабженных специальными методами уничтожения экземпляров классов (деструкторов, в них должно выполняться освобождение захваченных ресурсов) и автоматический вызов деструкторов в процессе выхода из программного блока (это уже делает компилятор).
Реализация объектно-ориентированного расширения
При разработке компилятора context 1.0 вопрос о реализациия классов не рассматривался. Я понимал, что это было бы желательно, но ничего не сделал. В данном случае никаких проблем не возникло - реализация классов естественным образом встраивается в язык и компилятор, но часто все бывает иначе - ограничения, заложенные при проектировании системы очень сложно преодолеть.
В описание языка Context добавлено три ключевых слова:
Первое слово может использоваться при объявлении метода класса, два других - внутри методов класса. Для объявления класса используется то же ключевое сово, что и для объявления структуры (struct). Реализация примерно (но не точно) соответствует Turbo Pascal. Приведенный выше пример класса CArray дает достаточное представление о синтаксисе. В отличие от Turbo Pascal, заголовок реализации метода должен точно повторять его объявление (слово virtual также должно быть повторено) - думаю, это улучшает читаемость текста. Пока никак не решен вопрос о создании экземпляра класса в свободной памяти, реализовано только автоматическое размещение экземплятов классов в области данных или в стеке - для вызовов виртуальных методов никакой явной инициализации экземпляра класса не требуется - инициализация выполняется посредством неявного вызова конструктора. Поля классов также инициализируются автоматически.
Заберая вперед перечислю дополнения, которые нужно внести в компилятор:
Запрет присваивания экземпляров полиморфных классов - не очень красивое решение, но оно предотвращает компиляцию следующей некорректной программы:
Ошибка в том, что экземпляру класса 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), но в ней и ассемблера было больше и, что важнее, использовался совсем иной алгоритм вывода на экран - с помощью рекурсивной процедуры окно, в которое производится вывод разбивалось на множество полностью открытых прямоугольных областей и каждая из них копировалась из буфера в видеопамять. Задача казалась сложной, а ее решение ее было простым и изящным... Но это отступление, тестовый пример содержит следующие классы (стрелками показаны отношения наследования):
Четыре класса в правой колонке (CChip-фишка, CPuzzle-головоломка, CClock-часы и CApp-приложение) являются объектами прикладной программы, Пример содержит три окна диалога, с помощью мыши можно перемещать их по экрану и менять их размеры.