© 2024
Александр Легалов
Павел Косов
|
Статья представляет собой подробное исследование процедурно-параметрического полиморфизма и его реализации в языке программирования C. Однако возникает вопрос об актуальности темы с учетом сложности адаптации нового подхода и широкого применения различных инструментов автоматической генерации кода.
Полная рецензия на непринятую статью по предлагаемым решениям и возможностям 4П, поданную на одну из конференций 2024 года.
|
1. Мотивация
Несмотря на ряд интересных особенностей, обеспечивающих гибкую разработку программ, процедурно-параметрическая парадигма (ППП) программирования (4П) не получила распространения. Возможно это связано с отсутствием соответствующих языков программирования, позволяющих непосредственно использовать предлагаемые ею решения. Реализация языка О2М [o2m-articleЛегалов А.И. Швец Д.А. Процедурный язык с поддержкой эволюционного проектирования. - Научный вестник НГТУ, № 2 (15), 2003. С. 25-38., o2m-site] на базе Оберона-2 [oberon-2The Programming Language Oberon-2 H.Moessenboeck, N.Wirth. Institut fur Computersysteme, ETH Zurich July 1996.], не нашла своего пользователя. Во-первых, языки данного семейства популярны в достаточно узком круге разработчиков. Во-вторых, сформированная реализация O2M позволила провести ряд базовых экспериментов, но не получила дальнейшего развития, раскрывающего более интересные возможности процедурно-параметрического программирования.
Нельзя сказать что процедурное программирование в настоящее время находится в полном упадке и забвении. Например, язык программирования C постоянно входит в пятерку наиболее популярных языков по различным рейтингам, что обуславливается его гибкостью и относительной простотой, а также использованием в предметных областях, где ОО подход не является эффективным. В связи с этим предлагается решение, направленное на включение в язык программирования C конструкций, поддерживающих ППП. Ниже рассматриваются дополнительные синтаксические конструкции, ориентированные на поддержку предлагаемого подхода. Описываются их семантика, возможности и особенности отображения в более простые конструкции, применяемые в архитектурных решениях уровня системы команд.
Использование языка C также связано с тем, что в нем нет ничего лишнего, что могло бы сделать предлагаемый механизм поддержки динамического полиморфизма избыточным. Из широко используемых языков программирования практически только C остается столь долгое время достаточно консервативным, что часто позволяет разработчикам новых языков издеваться над ним, добавляя различные рюшечки, изменяющие его характеристики. В целом мы старались не вносить таких изменений, которые не позволили бы компилировать обычные C-программы. Предлагаемые синтаксис и семантика обертывают конструкции языка C, не влияя на его традиционное использование. Данный документ предполагается поддерживать в актуальном состоянии по ходу дальнейшей разработки и добавления новых языковых конструкций.
2. Процедурно-параметрические конструкции, добавляемые в язык программирования C
Вводимые расширения языка C базируются на дополнительном включении в него ранее описанных конструкций и введения соответствующих понятий при их использовании [ppc-realisation, ppc-using]. К этим понятиям для данного языка относятся:
-
основа специализации;
-
специализация обобщения;
-
параметрическое обобщение;
-
экземпляр параметрического обобщения или специализированная переменная;
-
обобщающая функция;
-
обработчик параметрической специализации или специализированная функция;
-
вызов параметрической процедуры.
2.1. Основа специализации
В качестве основы специализации выступает любой уже существующий базовый или составной тип языка, имеющий имя. То есть, практически любой тип может использоваться в качестве специализации обобщения. Также в качестве основы может использоваться и ранее сформированное именованное параметрическое обобщение, которое по сути тоже является составным типом. Понятие основы не накладывает никаких ограничений на самостоятельное использование этих типов в программе и вводится только для терминологической стыковки с другими вводимыми понятиями. Они, как и ранее, остаются обычными типами.
В качестве примеров структур, используемых для иллюстрации основ специализаций, можно привести представления треугольника, прямоугольника и круга:
struct Triangle {int a, b, c;}; // Треугольник
struct Rectangle {int x, y;}; // Прямоугольник
struct Circle {int r;}; // Круг
2.2. Специализация обобщения
Специализация обобщения (или просто "специализация") формируется на основе обобщения языка C, определяющего головной тип, и подтипа, который может являться любой из основ специализации. Помимо этого подтип может задаваться в виде неименованной структуры или объединения, а также подключаться через указатель на любой именованный тип. Каждая специализация определяет в параметрическом обобщении один из альтернативных вариантов организации данных. По своей сути специализация близка по семантике к альтернативному полю объединения языка программирования C.
2.3. Параметрическое обобщение
Параметрическое обобщение (или просто <<обобщение>>) является структурой, определяющий головной тип для множества формируемых альтернатив, являющихся специализациями. В предлагаемом расширении языка C определение обобщения отличается от определения структуры наличием дополнительного поля, задаваемого в угловых скобках. В этом поле может быть описано от нуля до нескольких специализаций обобщения, определяющих множество альтернативных подтипов. Синтаксис обобщения можно описать следующим образом:
Обобщение = Структура "<" [СписокСпециализаций] ">" ["const"].
СписокСпециализаций = Специализации ";" { Специализации ";" }.
Специализации = ИмяТипа ";"
| Признак {"," Признак} ":" Тип ";"
{Признак {"," Признак} ":" Тип ";"}.
Признак = Идентификатор.
Тип = ИмяТипа
| ОписаниеСтруктурного типа
| ОписаниеНеименованногоТипа.
При определении обобщений также допускается использование конструкции typedef языка C. Отсутствие в обобщении списка специализаций предполагает его децентрализованное формирование в ходе последующей разработки программы.
Примечание. Возможно стоит добавить в описание обобщения варианта, когда не допускается обобщение без специализаций. Для этого можно воспользоваться тегом "=0". Тогда нельзя будет создавать переменные, содержащие только обобщение без специализаций.
Уникальность специализаций, имеющих общий головной тип, определяться уникальностью их признаков, которые могут задаваться явно или неявно. При неявном задании в качестве признака используется имя типа из основы специализации. В этом случае не допускается наличие нескольких специализаций одного типа с неявным заданием признака. При явном задании признаков допускается множество альтернатив имеющих одинаковый тип, а также использование в качестве типов неименованных структур и объединений, непосредственно определяемых при описании обобщения. Также в качестве типа специализации при явном задании ее признака допускается использование ключевого слова void.
Общая структура параметрического обобщения представлена на рисунке 1. Оно может содержать произвольное количество специализаций, добавляемых в различных единицах компиляции. Внешняя идентификация обобщения определяется его типом. Помимо этого для обобщения и его специализаций формируется локальная идентификация параметрическими индексами в диапазоне от 0 до n, где n - количество специализаций обобщения. При этом само обобщение имеет значение индекса, равное 0. Индексы от 1 до n присваиваются специализациям произвольно в зависимости от реализации компилятора, компоновщика или среды выполнения программы. В текущей версии формирование параметрических индексов осуществляется во время запуска программы.
Рисунок 1 – Структура параметрического обобщения
2.4. Расширение обобщения
Обобщение может расширяться путем добавления новых специализаций в различных единицах компиляции, что и отличает его от объединения языка C. Это обуславливается тем, что зачастую появление в программе новых специализаций может осуществляться не сразу, а в процессе ее инкрементальной разработки. В таких случаях целесообразно иметь возможность расширения набора специализаций обобщения по мере необходимости. Описание их подключения осуществляется в соответствии со следующими синтаксическими правилами:
ДобавлениеСпециализаций =
ИмяОбобщения "+" "<" СписокСпециализаций ">".
В качестве примера можно рассмотреть следующий вариант формирования обобщения и его специализаций. Пусть на первом этапе необходимо сформировать специализацию обобщения для треугольника. Это можно осуществить созданием следующей специализации:
struct Figure {}<struct Triangle;>;
Допускается использовать единую часть обобщенной структуры без данных, что указывает на отсутствие общей информации у всех формируемых обобщений. Последующее расширение обобщения может осуществляться как путем одиночного, так и группового включения новых специализаций. Например, добавление прямоугольника и круга может выглядеть следующим образом:
struct Figure + <struct Rectangle; struct Circle;>;
Первоначальное обобщение также можно создавать и без специализаций, то есть, пустым. В этом случае список специализаций будет отсутствовать. Для текущего примера первоначальное формирование специализации-треугольника может быть реализовано следующим образом:
// В обобщенной структуре отсутствуют специализации
struct Figure {}<>;
// Включение треугольника в обобщенную фигуру
struct Figure + <struct Triangle;>;
Использование идентификации посредством признаков специализаций позволяет многократно использовать один и тот же тип. Пусть структура прямоугольника используется для задания ромба через его диагонали, а для задания отрезка через его длину используем структуру, определяющую круг. Помимо этого определим дополнительные имена типов основ специализаций с применением typedef:
typedef struct Triangle {int a, b, c;} Triangle; // Треугольник
typedef struct Rectangle {int x, y;} Rectangle; // Прямоугольник
typedef struct Circle {int r;} Circle; // Круг
При изначально пустом наборе специализаций обобщение будет выглядеть следующим образом:
typedef struct Figure2 {}<> Figure2;
Дальнейшее расширение обобщения, можно описать следующим образом:
// Добавление прямоугольника, ромба, отрезка
Figure2 + <rect, rhomb: Rectangle; section: Circle;>;
// Добавление треугольника и круга
Figure2 + <trian: Triangle; circ: Circle;>;
На первом этапе обобщение Figure2 расширяется за счет прямоугольника, ромба (использование сторон основы специализации в качестве диагоналей ромба) и отрезка указанной длины (за счет вульгарного использования типа, определяющего круг). В следующим описании добавляются треугольник и круг. Расширения обобщения могут осуществляться в разных единицах компиляции, в каждой из которых будут видны только свои специализации. В общем случае, при сохранении однозначности, допускается в одном обобщении использовать явное и неявное задание признаков специализаций.
Если необходимо сформировать обобщение, которое в дальнейшем не должно изменяться, то для этого можно использовать квалификатор const, следующий непосредственно после описания специализаций в первоначальном определении обобщения. В качестве примера можно привести обобщенный тип, задающий дни недели:
// Дни недели
struct WeekDay {int week_number;}
< Monday, Tuesday, Wednesday, Thursday,
Friday, Saturday, Sunday: void;
> const;
В примере, за счет использования типа void, осуществляется имитация перечисления. Общее поле в структуре задает номер недели в году. Различные признаки, предшествующие типу, позволяют задать только семь альтернатив, с каждой из которой можно впоследствии связать свой полиморфный обработчик специализации. При отсутствии квалификатора const можно имитировать эволюционно расширяемый перечислимый тип.
2.5. Экземпляры параметрических обобщений
Экземпляры параметрических обобщений являются переменными, сформированными на основе описанных специализаций (специализированными переменными). Для каждой из таких переменных определен тип специализации, являющийся подтипом обобщения. При этом переменная, сформированная от чистого обобщения также является специализированной переменной и ее описание осуществляется без указания подтипа специализации. В ходе описания специализированных переменных допускается начальная инициализация. Возможно формирование как скалярных специализированных переменных, так и массивов различной размерности. Помимо этого можно задавать описание указателей на специализированные переменные, а также создавать массивы таких указателей. Указатели на специализации при этом могут ссылаться только на соответствующие специализированные переменные в отличие от указателей на параметрические обобщения, которые могут указывать на любые специализированные переменные, сформированные от соответствующего обобщения.
ОписаниеСпециализированнойПеременнной =
СсылкаНаОбобщеннуюСтруктуру ["." ( ИмяТипа | Признак ) ]
СписокСпециализированныхПеременных.
СписокСпециализированныхПеременных =
СпециализированнаяПеременная
{"," СпециализированнаяПеременная}.
СпециализированнаяПеременная =
{"*"} ИмяСпециализированнойПеременной {"[" Размерность "]"}
["=" Инициализатор].
СсылкаНаОбобщеннуюСтруктуру =
struct ИмяОбобщенной структуры | ИмяTypedef.
Размерность массива задается аналогично описанию размерности для обычных переменных языка C. Инициализация определяется в зависимости от размерности и типа формируемой переменной.
Допускается создание переменных непосредственно от обобщения. То есть переменных, имеющих только структурную часть при отсутствии части, определяемой ее специализацией. Указатели на обобщения могут ссылаться на любые специализации данного обобщения, что указывает на возможность разыменования. Соответствующие действия могут осуществляться в ходе начальной инициализации, выполнения операции присваивания, при передаче параметров в функции, возврате результатов из функции и в других аналогичных ситуациях.
ОписаниеУказателейНаОбобщения =
ТипОбобщения СписокОбобщенныхУказателей.
СписокОбобщенныхУказателей =
ОбобщенныйУказатель {"," ОбобщенныйУказатель}.
ОбобщенныйУказатель =
"*"{"*"} ИмяОбобщенногоУказателя {"[" Размерность "]"}
["=" Инициализатор].
Как и любые переменные языка C, обобщенные и специализированные переменные могут быть глобальными, статическими, локальными или динамическими. Их значения могут использоваться в качестве фактических параметров при вызове функций.
В качестве примера можно привести следующие варианты создания специализированных переменных и обобщенных указателей:
struct Figure.Triangle t1 = {}@{3,4,5}, t2;
struct Figure.Circle c1 = {}@{10}, *pc1 = &c1, c[10];
Figure2.rect r1, r2[3] = {{}@{1,2},{}@{3,4},{}@{5,6}},
r3 = @{7,4}, pr1 = &r1;
struct Figure *pf1 = &t1, **ppf1 = &pf1;
struct WeekDay.Monday monday = {35}; // Меняется только неделя
К особенностям инициализации можно отнести описание значений основ специализаций после символа "@". При этом для структурных значений необходимо также указывать дополнительные фигурные скобки, которые не нужны, если специализация является скаляром.
Примечание. В настоящий момент инициализация специализированных переменных еще до конца не реализована. Поэтому пока предлагается использовать для установки требуемых значений операторы присваивания.
Рекурсивное расширение специализаций
Построение новых специализаций может также осуществляться на основе параметрического обобщения, что позволяет формировать цепочки уточнений произвольной длины. При этом следует отметить, что подобное создание новых специализаций возможно только в том случае, если предшествующий тип является обобщенным. Это позволяет контролировать процесс добавления новых уточнений во время компиляции. Например, для формирования новой ступени обобщения T можно включить в него обобщение T0:
struct T {int x, y;}<>;
struct T + <t0: _Bool; t1: struct{double r; char s;}>;
struct T0 {int z;}<>;
struct T + <t2: T0;>;
Тип T0 можно будет уточнять, добавляя для этого к нему новые специализации, которые также могут содержать обобщения, обеспечивающие их дальнейшее уточнение:
struct T00 {int a;}<>;
struct T0 + <t00: T00;>;
Использование данного приема позволяет выстраивать сложные зависимости между типами, воспринимая при этом различные специализации как уточнения одного и того же типа. Для приведенного примера могут быть сформированы следующие специализации:
-
из T ⇒ T.t0, или T.t1 или T.t2,
-
из T.t2 ⇒ T.t2.t00 и т.д.
Допускается также рекурсивное подключение к существующим обобщениям других обобщений, включая и подключение самого себя. При необходимости это позволяет выстраивать длинные константные цепочки, формируемые во время компиляции программы. В качестве примера можно расширить тип T специализацией, построенной на основе этого же типа:
struct T + <t3: T>;
T + t3: T;
Появляется возможность выстраивать специализации, обеспечивающие поддержку возможностей, эквивалентных декорированию:
T.t3, T.t3.t3, T.t3.t3.t3.t3.t2.t00, ...
Представленный подход можно использовать, например, при процедурно-параметрической реализации ОО паттерна Декоратор.
Операции над специализированными переменными
Над специализированными переменными, а также над специализированными и обобщенными указателями возможны операции доступа к отдельным полям для чтения данных и присваивания. Допускается манипуляция над отдельными полями как структурной части, так и специализации. При этом для полей структурной части специализированной переменной в качестве разделителя выступает, как обычно, точка ("."). Поле обобщенной части отделяется от обозначения переменной знаком "@". При обращении через указатель используется знак "->". Например:
t1.@a = 5;
t2 = t1;
pf1 = pc1;
monday.week_number = 24; // Понедельник 24-й недели
struct T.t0 b = {}@1; // Инициализация специализации базового типа
b.@ = 0; // Изменение специализации базового типа
pc1->@r = 10; // Изменение радиуса в специализации круга
В случаях, когда поля специализаций являются структурами, обращение к ним осуществляется с указанием имени поля, перед которым ставится точка. Обращение к структурной части специализированной переменной осуществляется также как и к обычной структурной переменной.
2.6. Обобщающие параметрические функции
Обобщающие функции обеспечивают поддержку процедурно-параметрического полиморфизма. Их сигнатуры определяют единый интерфейс для обработчиков параметрических специализаций. Каждая обобщающая функция имеет от одного до нескольких обобщенных формальных параметров. Они задаются в виде указателей на обобщения. При вызове такой функции в нее передаются ссылки на специализированные переменные, комбинации которые и определяют конкретный обработчик специализации. Обобщающая параметрическая функция описывается следующими синтаксическими правилами:
ОбобщающаяФункция = ТипВозвращаемогоПараметра ИмяФункции
"<" СписокОбобщенныхПараметров ">"
"(" [ СписокФормальныхПараметров ] ")"
ТелоОбобщающейФункции.
СписокОбобщенныхПараметров =
ОбобщенныйПараметр {"," ОбобщенныйПараметр }.
ТелоОбобщающейФункции = ПустоеТело | ОбработчикПоУмолчанию.
ПустоеТело = "=" "0".
ОбобщенныйПараметр = ТипОбобщения "*" ИмяОбобщенногоПараметра.
Например, пустая обобщающая функция вывода на печать геометрической фигуры может быть представлена следующим образом:
void PrintFigure<struct Figure *f>() = 0;
Обобщенные параметры задаются в угловых скобках (в отличие от прочих параметров, которые, как обычно, располагаются в круглых скобках при их наличии). Отсутствие тела в данном случае говорит о пустой обобщающей функции. Это означает, что необходимо написать обработчики для всех специализаций. В данном случае для всех конкретных фигур, связанных с соответствующим параметрическим обобщением. Вместо пустой обобщающей функции может быть задан обработчик по умолчанию, который вызывается в тех случаях, когда для подставляемой комбинации специализаций не будет реализован свой обработчик. Простейшим вариантов подобного обработчика в таком случае может служить вызов функции прерывания программы:
void PrintFigure<struct Figure *f>() {
printf("Incorrect Argument!!!\n")
exit(-1);
}
Предполагается, что перед вызовами обобщающих функций компилятором будет автоматически генерироваться код, проверяющий наличие всех указателей на обработчики специализаций или, при их отсутствии, на заменяющий их обработчик по умолчанию.
2.7. Обработчики параметрических специализаций
Обработчики параметрических специализаций (или специализированные функции) вызываются при обращении к обобщающей функции. Они обеспечивают непосредственную манипуляцию конкретными специализациями, включенными в состав параметрических обобщений. Синтаксис обработчиков:
СпециализированнаяФункция =
ТипВозвращаемогоПараметра ИмяФункции
"<" СписокСпециализаций ">" "(" СписокФормальных параметров ")"
ТелоСпециализированнойФункции.
СписокСпециализаций =
СпециализированныйПараметр {"," СпециализированныйПараметр }.
СпециализированныйПараметр = ТипСпециализации "*" ИмяСпециализации.
Выбор обработчика осуществляется в зависимости от значений параметрических аргументов, подставляемых вместо формальных параметров. Они могут располагаться в разных единицах компиляции, добавляясь, например, по мере создания очередной специализации, и для обобщающей функции печати фигуры выглядеть следующим образом:
// Вывод параметров прямоугольника
void PrintFigure<struct Figure.Rectangle *r>() {
printf("Rectangle: x = %d, y = %d", r->@x, r->@y);
}
// Вывод параметров треугольника
void PrintFigure<struct Figure.Triangle *t>() {
printf("Triangle: a = %d, b = %d, c = %d",
(*t).@a, (*t).@b, (*t).@c);
}
// Вывод параметров круга
void PrintFigure<struct Figure.Circle *r>() {
printf("Circle: r = %d", c->@r);
}
Использование нескольких обобщенных аргументов
Основное достоинство процедурно-параметрической парадигмы проявляется при эволюционном расширении в случае множественного полиморфизма, определяющего реализацию мультиметодов. Предлагаемое расширения языка C позволяет безболезненно расширять мультиметоды, добавляя в них новые специализации независимо от числа обобщенных аргументов. В качестве примера можно рассмотреть мультиметод, проверяющий возможность размещения фигуры, задаваемой первым аргументов внутри фигуры, задаваемой вторым аргументом. Пусть изначально для обобщения Figure2, описанного выше, имеется информация только о специализациях прямоугольника и треугольника, заданных с использованием признаков и описанием typedef. В этом случае обобщающая функция может иметь следующий вид:
// Обобщенная функция, требующая обязательного определения
// мультиметода для всех специализаций
_Bool Multimethod() = 0;
Для ее видимости в других единицах компиляции, используемых для определения обработчиков специализаций достаточно прототипа, описанного, например, в заголовочном файле:
_Bool Multimethod();
В одной или нескольких единицах компиляции можно сформировать четыре функции, осуществляющие обработку различных комбинаций специализаций. Каждый обработчик специализации описывает одну комбинацию параметров и определяет метод ее обработки:
// Прямоугольник разместится внутри прямоугольника
_Bool Multimethod<Figure2.rect *r1, Figure2.rect *r2>() {
return ((r1->@x < r2->@x) && (r1->@y < r2->@y)) ||
((r1->@x < r2->@y) && (r1->@y < r2->@x));
}
// Прямоугольник разместится внутри треугольника
_Bool Multimethod<Figure2.rect *r1, Figure2.trian *t2>() {...}
// Треугольник разместится внутри прямоугольника
_Bool Multimethod<Figure2.trian *t1, Figure2.rect *r2>() {...}
// Треугольник разместится внутри треугольника
_Bool Multimethod<Figure2.trian *t1, Figure2.trian *t2>() {...}
Более сложные алгоритмы заменены многоточием. Приведенный пример показывает, что специализации, тип которых известен во время компиляции, обрабатываются так же, как и обычные переменные эквивалентного типа.
Добавление добавление в программу специализаций для новых геометрических фигур осуществляется без изменений существующих единиц компиляции. Например, появление круга, приведет к реализации дополнительных обработчиков специализаций в новых файлах, подключаемых к проекту:
// Прямоугольник разместится внутри круга
_Bool Multimethod<Figure2.rect r1*, Figure2.circ *с2>() {
return ((r1->@x*r1->@x + r1->@y*r1->@y) < (c2->@r*c2->@r));
}
// Треугольник разместится внутри круга
_Bool Multimethod<Figure2.trian *t1, Figure2.circ *с2>() {...}
// Круг разместится внутри прямоугольника
_Bool Multimethod<Figure2.circ *c1, Figure2.rect *r2>() {...}
// Круг разместится внутри треугольника
_Bool Multimethod<Figure2.circ *c1, Figure2.trian *t2>() {...}
// Круг разместится внутри круга
_Bool Multimethod<Figure2.circ *c1, Figure2.circ *c2>() {
return c1->@r < c2->@r;
}
2.8. Вызовы параметрических функций
Вызов функции по сути запускает обработчик для конкретной комбинации специализаций. Он отличается от вызова обычной функции только наличием списка дополнительных фактических параметров, указывающих на специализации, по которым автоматически осуществляется выбор обработчика, что и определяет динамический полиморфизм. Вызов параметрической функции имеет следующий синтаксис:
ВызовПараметрическойФункции = ИмяФункции
"<" СписокУказателейНаСпециализации ">"
"(" [СписокФактическихПараметров] ")".
В качестве примеров можно привести вызов функции печати фигуры для различных выше описанных специализированных переменных:
// печать параметров треугольника t1
PrintFigure<&t1>();
// печать параметров круга c1
PrintFigure<&c1>();
// печать параметров круга c1 через указатель pc1
PrintFigure<pc1>();
// печать параметров круга c[5]
PrintFigure<&c[5]>();
// печать параметров круга c[5] через смещение указателя
PrintFigure<c+5>();
// печать треугольника t1 через обобщенный указатель pf1
PrintFigure<pf1>();
// печать треугольника t1 через указатель на указатель *ppf1
PrintFigure<*ppf1>();
Аналогичным образом используются вызовы параметрических функций и для нескольких обобщенных параметров. То есть в случае описанных выше мультиметодов, вызовы параметричечких функций могут выглядеть следущим образом:
// Сопоставление треугольника t1 и круга c1
Multimethod<&t1, &c1>()
// Сопоставление круга подключенного через указатель pc1 и круга c1
Multimethod<pc1, &c1>()
// Сопоставление круга c[5] и фигуры подключенной через pf1
Multimethod<&c[5], pf1>()
3. Дополнительная поддержка с использованием специальных функций
Основная идея, связанная с расширением языка C механизмом поддержки процедурно-параметрического полиморфизма связана с минимальным влиянием на сам язык. То есть предполагается отсутствие каких-либо дополнительных механизмов, влияющих на семантику языка программирования C. Однакое для реализации предлагаемой семантики во время выполнения программы необходима ее дополнительная поддержка, которая реализована за счет введения дополнительных функций. Представленные ниже функции манипулируют именами типов по аналогии с функцией sizeof. Поэтому относятся к языковым конструкциям, разбираемым во время компиляции.
3.1. Инициализация областей памяти процедурно-параметрическими типами
Явное создание глобальных, статических и локальных переменных осуществляется во время компиляции и средой выполнения автоматически. Основной операцией при этом является установка признака специализации. Однако язык C допускает использование для хранения данных неименованных областей, которые могут быть приведены к желаемому виду путем явного назначения типа. Несмотря на ненадежность подобной операции, имеет смысл обеспечить ее поддержку для переменных специализированных переменных. Для этого реализована функция инициализации специализаций, имеющая следующий формат:
ИнициализацияСпециализации =
bool init_spec "(" ИмяОбобщения["." ПризнакСпециализации]
"," УказательНаОбластьПамяти ")".
Функция позволяет осуществить размещение нужной специализации в области памяти, выделенной любым способом и по смыслу соответствует любой из функций языка C, обеспечивающей явное приведение к нужному типу. Необходимость в отдельной функции связана с тем, что помимо именования указателя на область памяти нужно еще и установить признак при создании требуемой специализации.
Функция является ненадежной, но в целом полностью соответствует концепциям языка программирования C. Она также позволяет формировать массивы, состоящие из специализированных переменных инициализируя в цикле признаками специализаций соответствующие области памяти.
3.2. Работа с динамически выделяемой памятью
Для размещения специализированных переменных в динамической памяти в целом достаточно использовать функцию init_spec совместно с библиотечными функциями языка C и его оператором sizeof. Например, для выделения памяти под прямоугольник можно выполнить следующие действия:
Figure2* pfr = malloc(sizeof(Figure2.rect));
init_spec(Figure2.rect, pfr);
Функция malloc обеспечивает выделение памяти под специализацию Figure2.rect, размер которой определяется оператором sizeof. Инициализация заданной области необходима для установки признака специализации и обеспечивается функцией init_spec. Освобождение выделенной памяти осуществляется как обычно функцией free.
Учитывая то, что динамическое выделение памяти встречается достаточно часто, для удобства работы с ней реализована функция create_spec, которая по сути является синтаксическим сахаром над приведенными выше операторами. Она имеет следующий формат:
СозданиеСпециализации =
УказательНаОбобщение create_spec
"(" ИмяОбобщения["." ПризнакСпециализации] ")".
Данная функция возвращает указатель на выделенную область памяти в виде указателя на обобщение. Ее использование позволяет записать выделение памяти для прямоугольника следующим образом:
Figure2* pfr = create_spec(Figure2.rect);
3.3. Функция для определения числа специализаций
В отличие от других механизмов поддержки динамического полиморфизма процедурно-параметрический полиморфизм позволяет получить во время выполнения информацию о количестве специализаций обобщения. Это возможно вследствие локальной идентификации подтипов обобщения числами от 0 до N, задающих параметрические индексы обобщения и его специализаций. Таким образом, общее количество альтернатив, которые могут выступать в роли специализаций, может изменяться от 1 до N, а общее число альтернатив, которые могут использоваться в вычислениях равно N+1.
Это дает возможность реализовать ряд нестандартных подходов к программированию с использованием индексов специализаций. В частности, во время выполнения программы можно получить количество специализаций, воспользовавшись функцией, возвращающей их число, включая и само обобщение, которое в ряде случаев тоже может использоваться в вычислениях:
ЧислоСпециализаций =
int get_spec_size "(" ИмяОбобщения ")".
3.4. Получение указателя на специализацию по параметрическому индексу
Зная количество специализаций, можно использовать их индексы без боязни выйти за максимальное число. Например, можно решить обратную задачу: по индексу специализации получить указатель на соответствующую специализированную переменную. Для этого можно воспользоваться специальной функцией возвращающей указатель на специализацию через указатель на обобщение:
ПолучениеУказателяНаСпециализацию =
УказательНаОбобщение get_spec_ptr
"(" ИмяОбобщения, ИндексСпециализации ")"
Эти константные указатели на фиктивные специализации могут формироваться во время компиляции и не использоваться в ходе вычислений. Их основной задачей является использование в качестве параметрических аргументов для выбора соответствующих обработчиков специализаций, в которых их значение при этом не используется.
3.5. Сравнение на равенство двух альтернатив одного обобщения
Использование параметрических индексов позволяет достаточно легко реализовать сравнение двух альтернатив, принадлежащих одному обобщению, на равенство специализаций. Для этого достаточно, чтобы индексы сравниваемых переменных совпадали. Для этого используется функция, имеющая следующий формат:
ИдентичностьСпециализаций =
int spec_index_cmp
"(" УказательНаОбобщение, УказательНаОбобщение ")"
Оба указателя должны иметь один и тот же тип обобщения, что легко определяется во время компиляции.
В результате сравнения возвращается целое число, которое может принимать следующие значения:
-
При совпадении специализаций возвращается значение от 0 до N, где N - количество специализаций, включая само обобщение. Возвращаемое значение является индексом проверяемой специализации.
-
Если специализации не совпадают, то возвращается значение, равное -1.
Помимо сравнение на совпадение двух неизвестных специализаций данная функция может использоваться в ситуациях, когда одна из специализаций выступает в роли известного образца. Это позволяет осуществить явную проверку подтипа обобщения во время выполнения программы.
Заключение
Предлагаемые расширения языка программирования C конструкциями, обеспечивающими инструментальную поддержку процедурно-параметрической парадигмы программирования, поддерживают гибкую разработку программ. При этом обеспечивается добавление новых альтернативных данных, а также функций, позволяющих безболезненно для ранее написанного кода использовать множественный полиморфизм.
Реализация расширенией осуществляется путем их интеграции в компилятор clang [clang], что позволяет изменять как синтаксический анализатор, так и абстрактное синтаксическое дерево, адаптируя их под новые конструкции. Проект реализуется под открытой лицензией и размещен на Гитхаб [ppclang]. Примеры процедурно-параметрических программ также размещены на Гитхаб [ppc-examples]. Ряд примеров также рассмотрены в публикациях [ppc-realisation, ppc-using].
Список используемых источников
Легалов А.И. Швец Д.А. Процедурный язык с поддержкой эволюционного проектирования. - Научный вестник НГТУ, № 2 (15), 2003. С. 25-38.
Легалов А.И. Швец Д.А. Язык программирования О2М(2003-2004).
The Programming Language Oberon-2 H.Moessenboeck, N.Wirth. Institut fur Computersysteme, ETH Zurich July 1996.
Легалов А.И., Косов П.В. Расширение языка C для поддержки процедурно-параметрического полиморфизма. Моделирование и анализ информационных систем. 2023;30(1):40-62. https://doi.org/10.18255/1818-1015-2023-1-40-62.
Легалов А.И., Косов П.В. Процедурно-параметрический полиморфизм в языке C для повышения надежности программ. // XIV Всероссийское совещание по проблемам управления, ВСПУ-2024, Москва 17-20 июня 2024 г. С. 2827-2833. URL: https://vspu2024.ipu.ru/preprints/2827.pdf
Clang: A c language family frontend for llvm.
Размещение проекта с процедурно–параметрической версией языка программирования C на Гитхаб.
Примеры процедурно-параметрических программ и их сравнение с программами, написанными с использованием других парадигм программирования.
|