|
© 2026
Александр Легалов
Павел Косов
|
|
πάντα ρεῖ καὶ οὐδὲν μένει
(всё течёт и ничто не остаётся на месте)
античный фразеологизм.
|
1. Новый поворот...
Данный документ описывает изменения, которые внесены в следующую версию процедурно-параметрического C (PPC). Появление нового документа, а не изменение уже существующего связано с пересмотром синтаксиса, который имеет значительные отличия от ранее разработанного при прежней, но дополнительно расширенной семантике. Предыдущий вариант синтаксиса почти полностью поменялся на новый. Но пока ранее разработанные программы не исправлены в репозиториях, а их использование в различных текстах остается, описание прежнего синтаксиса не убирается. Оно только помечено как устаревшее. Изменения связаны с попытками сделать синтаксис более приближенным к языку C и упростить восприятие процедурно-параметрических конструкций, изменив в ряде случаев их трактовку. Также в PPC добавлены новые конструкции, которые можно считать синтаксическим сахаром. При этом, чтобы не заглядывать в предыдущее описание, в этом тексте дается полное представление о конструкциях поддерживающих процедурно-параметрическое программирование (ППП). Дополнительные комментарии поясняют отличия от предыдущих решений с попытками обосновать, почему они были приняты.
Следует также отметить, что старый вариант синтаксиса пока еще не убран из компилятора ppclang, который также начинает поддерживать некоторые особенности нового синтаксиса. Но планируется полный переход на новый синтаксис. Только новый синатаксис поддерживает компилятор ppchibicc, разработанный на основе компилятора chibicc с применением ИИ. Однако данный компилятор скорее всего можно трактовать как учебный из-за отсутствия поддержки некоторой функциональности, присущей промышленным решениям. Вместе с тем, в нем реализованы все конструкции, представленные в этом документе. Он легко устанавливается в различных дистрибутивах ОС Linux, что позволяет ознакомиться с особенностями процедурно-параметрического программирования. Также в ppchibicc планируется добавлять новые решения в ходе дальнейших экспериментов с ППП.
Как и предыдущий, это рабочий документ, который будет обсуждаться и дорабатываться в ходе окончательного формирования синтаксиса и семантики.
2. Процедурно-параметрические конструкции, добавляемые в язык программирования C
Вводимые расширения языка C базируются на дополнительном включении в него ранее описанных конструкций и введения соответствующих понятий при их использовании [1, 2]. К этим понятиям для данного языка относятся:
-
основа специализации;
-
специализация обобщения;
-
параметрическое обобщение;
-
экземпляр параметрического обобщения или специализированная переменная;
-
обобщающая функция;
-
обработчик параметрической специализации или специализированная функция;
-
вызов параметрической процедуры.
2.1. Основа специализации
В качестве основы специализации выступает любой уже существующий базовый или составной тип языка, имеющий имя. То есть, практически любой тип может использоваться в качестве специализации обобщения. Также в качестве основы может использоваться и ранее сформированное именованное параметрическое обобщение, которое по сути тоже является составным типом. Понятие основы не накладывает никаких ограничений на самостоятельное использование этих типов в программе и вводится только для терминологической стыковки с другими вводимыми понятиями. Они, как и ранее, остаются обычными типами.
В качестве примеров структур, используемых для иллюстрации основ специализаций, можно привести представления треугольника, прямоугольника и круга:
struct Triangle {int a, b, c;}; // Треугольник
struct Rectangle {int x, y;}; // Прямоугольник
struct Circle {int r;}; // Круг
2.2. Специализация обобщения
Специализация обобщения (или просто "специализация") формируется на основе обобщения языка PPC, определяющего головной тип, и подтипа, который может являться любой из основ специализации. Помимо этого подтип может задаваться в виде неименованной структуры или объединения, а также подключаться через указатель на любой именованный тип. Каждая специализация определяет в параметрическом обобщении один из альтернативных вариантов организации данных. По своей сути специализация близка по семантике к одному альтернативному полю объединения языка программирования C.
2.3. Параметрическое обобщение
Параметрическое обобщение (или просто "обобщение") является структурой, определяющий головной тип для множества альтернативных подтипов, образующих вместе с обобщением различные специализации. В общем случае обобщение может описываться несколькими раздельными конструкциями, первая из которых задает его определение, а последующие определяют расширения обобщения.
Определение обобщения строится на основе обычной структуры языка C, но с наличием дополнительного расширения, с использованием которого могут задаваться альтернативные специализации. Существуют базовое определение обобщения и его расширения, являющиеся по сути "синтаксическим и семантическим сахаром". Базовое определение продолжает определение структуры ключевым словом case, за которым в фигурных скобках идут описание подтипов, определяющих специализации. Может быть заданно от нуля до нескольких альтернативных подтипов. Синтаксис базового обобщения можно описать следующим образом:
Обобщение = "struct" Имя "{" [СписокЭлементовСтруктуры] "}"
"case" "{" ["default" Подтип ";"] [СписокПодтипов] "}"
["const"].
СписокПодтипов = Подтипы ";" { Подтипы ";" }.
Подтипы = [Тип ["*"]] Признак {"," ["*"] Признак }.
Подтип = Тип ["*"] Признак.
Признак = Идентификатор.
Тип = (ИмяТипа | "void") | СтруктурнаяМетка | МеткаОбъединения |
НеименованнаяСтруктура | НеименованноеОбъединение.
СтруктурнаяМетка = "struct" ИмяТипа.
МеткаОбъединения = "union" ИмяТипа.
Использование в качестве типа ключевого слова void предполагает, что у формируемого подтипа будут отсутствовать данные. Он будет содержать только признак, что можно трактовать как помеченный эволюционно расширяемый перечислимый тип. Помио этого эволюционно расширяемый перечислимый тип можно определять, если задавать только признак без типа. Такой вариант тоже допустим.
Использование опции, задаваемой ключевым словом const, позволяет зафиксировать определение обобщения, запрещая его дальнейшее расширение. Это бывает полезным в тех ситуациях, когда набор альтернатив жестко зафиксирован.
Использование ключевого слова default позволяет создавать исходное обобщение (трактуемое также как нулевая специализация) с дополнительной структурой. В противном случае нулевая специализация содержит только структурную часть.
При определении обобщений, как и в случае любых типов языка C, также допускается использование конструкции typedef. Отсутствие в обобщении списка подтипов предполагает децентрализованное формирование специализаций в ходе последующей разработки программы. Такое добавление специализаций возможно в различных единицах компиляции, где доступно исходное описание обобщения.
Уникальность специализаций, имеющих общий головной тип, определяться уникальностью признаков, которые задают имена подтипов. Явное задание признака подтипа позволяет использовать его в качестве поля в специализированной переменной для доступа к содержимому основы специализации. Признак подтипа является уникальным идентификатором к контексте одного обобщения. Также имя признака не должно совпадать с именами полей общей структуры, которая может также присутствовать в обобщении. Но это легко контролируется, так как при добавлении специализации все поля обобщенной структуры известны. Явное задание признаков специализаций допускает множество альтернативных подтипов, имеющих одинаковый тип основы специализации.
Общая структура параметрического обобщения представлена на рисунке 1. Оно может содержать произвольное количество специализаций, добавляемых в различных единицах компиляции. Внешняя идентификация обобщения определяется его типом. Помимо этого для обобщения и его специализаций формируется локальная идентификация параметрическими индексами в диапазоне от 0 до n, где n - количество специализаций обобщения. При этом само обобщение имеет значение индекса, равное 0, и называется нулевой специализацией. Индексы от 1 до n присваиваются специализациям произвольно в зависимости от реализации компилятора, компоновщика или среды выполнения программы.
Рисунок 1 – Структура параметрического обобщения
2.4. Расширение обобщения
Обобщение может расширяться путем добавления новых специализаций в различных единицах компиляции. Это отличает его от объединения языка C. Зачастую появление в программе новых специализаций может осуществляться не сразу, а в процессе ее инкрементальной разработки. В таких случаях целесообразно иметь возможность формирования специализаций обобщения по мере необходимости. Описание их подключения осуществляется в соответствии со следующими синтаксическими правилами:
ДобавлениеСпециализаций =
ИмяОбобщения "+" "{" СписокПодтипов "}".
В качестве примера можно рассмотреть следующий вариант формирования обобщения и его специализаций. Пусть на первом этапе необходимо сформировать специализацию обобщения для основы, представленной структурой треугольника. Это можно осуществить созданием следующей специализации:
struct Figure {} case {struct Triangle triangle;};
Допускается использовать единую часть обобщенной структуры без данных, что указывает на отсутствие общей информации у всех формируемых обобщений. Последующее расширение обобщения может осуществляться как путем одиночного, так и группового включения новых подтипов. Например, добавление прямоугольника и круга может выглядеть следующим образом:
struct Figure + {struct Rectangle rectangle; struct Circle circle;};
Первоначальное обобщение также можно создавать и без специализаций, то есть, пустым. В этом случае список специализаций будет отсутствовать. Для текущего примера первоначальное формирование специализации-треугольника может быть реализовано следующим образом:
// В обобщенной структуре отсутствуют специализации
struct Figure {} case {};
// Включение треугольника в обобщенную фигуру
struct Figure + {struct Triangle triangle;};
Использование идентификации посредством признаков специализаций позволяет многократно использовать один и тот же тип. Например, используем структуру прямоугольника для задания ромба через его диагонали, а для задания отрезка через его длину применим структуру, определяющую круг. Помимо этого определим дополнительные имена типов основ специализаций с применением typedef, чтобы не использовать ключевое слово struct:
typedef struct Triangle {int a, b, c;} Triangle; // Треугольник
typedef struct Rectangle {int x, y;} Rectangle; // Прямоугольник
typedef struct Circle {int r;} Circle; // Круг
При изначально пустом наборе специализаций обобщение будет выглядеть следующим образом:
typedef struct Figure2 {} case {} Figure2;
Дальнейшее расширение обобщения, можно описать следующим образом:
// Добавление прямоугольника, ромба, отрезка
Figure2 + {Rectangle rect, rhomb; Circle section;};
// Добавление треугольника и круга
Figure2 + {Triangle trian; Circle circ;};
На первом этапе обобщение Figure2 расширяется за счет прямоугольника, ромба (использование сторон основы специализации в качестве диагоналей ромба) и отрезка указанной длины (за счет вульгарного использования типа, определяющего круг). В следующим описании добавляются треугольник и круг. Расширения обобщения могут осуществляться в разных единицах компиляции, в каждой из которых будут видны только свои специализации.
Если необходимо сформировать обобщение, которое в дальнейшем не должно изменяться, то для этого можно использовать квалификатор const, следующий непосредственно после описания специализаций в первоначальном определении обобщения. В качестве примера можно привести обобщенный тип, задающий дни недели:
// Дни недели
struct WeekDay {
int week_number;
} case {
void Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday;
} const;
В примере рассматривается использование перечисления. Общее поле в структуре задает номер недели в году. Различные признаки, предшествующие типу, позволяют задать только восемь ал ьтернатив, включая нулевую специализацию, с каждой из которой можно впоследствии связать свой полиморфный обработчик специализации. При отсутствии квалификатора const можно имитировать эволюционно расширяемый перечислимый тип.
2.5. Экземпляры параметрических обобщений
Экземпляры параметрических обобщений являются переменными, сформированными на основе специализаций (специализированными переменными). Для каждой из таких переменных определен тип специализации, являющийся подтипом обобщения. При этом переменная, сформированная от чистого обобщения, также является специализированной переменной (нулевой специализированной переменной). Ее описание может осуществляться без указания подтипа специализации или в качестве подтипа может выступать ключевое слово void. В ходе описания специализированных переменных допускается их начальная инициализация. Она осуществляется в соответствии с правилами языка C, используемого для инициализации структур. Возможно формирование как скалярных специализированных переменных, так и массивов различной размерности. Помимо этого можно задавать описание указателей на специализированные переменные, а также создавать массивы таких указателей. Указатели на специализации при этом могут ссылаться только на соответствующие специализированные переменные в отличие от указателя на нулевую специализированную переменную (обобщенный указатель), который может указывать на любые специализированные переменные, сформированные от соответствующего обобщения.
ОписаниеСпециализированнойПеременнной =
СсылкаНаОбобщение ["." ( "void" | Признак ) ]
СписокСпециализированныхПеременных.
СписокСпециализированныхПеременных =
СпециализированнаяПеременная
{"," СпециализированнаяПеременная}.
СпециализированнаяПеременная =
{"*"} ИмяСпециализированнойПеременной {"[" Размерность "]"}
["=" Инициализатор].
СсылкаНаОбобщение =
"struct" ИмяОбобщеннойСтруктуры | ИмяTypedef.
Размерность массива задается аналогично описанию размерности для обычных переменных языка C. Инициализация определяется в зависимости от размерности и типа формируемой переменной.
Обобщенные указатели могут ссылаться на любые специализации данного обобщения, что указывает на возможность разыменования. Соответствующие действия могут осуществляться в ходе начальной инициализации, выполнения операции присваивания, при передаче параметров в функции, возврате результатов из функции и в других аналогичных ситуациях.
ОписаниеУказателейНаОбобщения =
ТипОбобщения СписокОбобщенныхУказателей.
СписокОбобщенныхУказателей =
ОбобщенныйУказатель {"," ОбобщенныйУказатель}.
ОбобщенныйУказатель =
"*"{"*"} ИмяОбобщенногоУказателя {"[" Размерность "]"}
["=" Инициализатор].
Как и любые переменные языка C, обобщенные и специализированные переменные могут быть глобальными, статическими, локальными или динамическими. Их значения могут использоваться в качестве фактических параметров при вызове функций.
В качестве примера можно привести следующие варианты создания специализированных переменных и обобщенных указателей:
struct Figure.triangle t1, t2;
struct Figure.circle c1, *pc1 = &c1, c[10];
Figure2.rect r1, r2[3], r3, *pr1 = &r1;
struct Figure *pf1 = &t1, **ppf1 = &pf1;
struct WeekDay.Monday monday = {35}; // Меняется только неделя
Рекурсивное расширение специализаций
Построение новых специализаций может также осуществляться на основе параметрического обобщения, что позволяет формировать цепочки уточнений произвольной длины. При этом следует отметить, что подобное создание новых специализаций возможно только в том случае, если предшествующий тип является обобщенным. Это позволяет контролировать процесс добавления новых уточнений во время компиляции. Например, для формирования новой ступени обобщения T можно включить в него обобщение T0:
struct T {int x, y;} case{};
struct T + {int t0; struct{double r; char s;} t1};
struct T0 {int z;} case{};
struct T + {struct T0 t2;};
Тип T0 можно будет уточнять, добавляя для этого к нему новые специализации, которые также могут содержать обобщения, обеспечивающие их дальнейшее уточнение:
struct T00 {int a;} case {};
struct T0 + {struct T00 t00;};
Использование данного приема позволяет выстраивать сложные зависимости между типами, воспринимая при этом различные специализации как уточнения одного и того же типа. Для приведенного примера могут быть сформированы следующие специализации:
-
из
T ⇒ T.t0, или T.t1 или T.t2,
-
из
T.t2 ⇒ T.t2.t00 и т.д.
Допускается также рекурсивное подключение к существующим обобщениям других обобщений, включая и подключение самого себя. При необходимости это позволяет выстраивать длинные константные цепочки, формируемые во время компиляции программы. В качестве примера можно расширить тип T специализацией, построенной на основе этого же типа:
struct T + {struct T t3;};
Появляется возможность выстраивать специализации, обеспечивающие поддержку возможностей, эквивалентных декорированию:
T.t3, T.t3.t3, T.t3.t3.t3.t3.t2.t00, ...
Представленный подход можно использовать, например, при процедурно-параметрической реализации ОО паттерна Декоратор без применения косвенного связывания.
Операции над специализированными переменными
Над специализированными переменными, а также над специализированными и обобщенными указателями возможны операции доступа к отдельным полям для чтения данных и присваивания. Допускается манипуляция над отдельными полями как структурной части, так и специализации. При этом для полей структурной части специализированной переменной в качестве разделителя выступает, как обычно, точка ("."). Поле обобщенной части становится доступной через имя признака специализации, следующего после точки. При обращении через указатель вместо точки используется знак "->". Например:
t1.triangle.a = 5;
t2 = t1;
pf1 = pc1;
monday.week_number = 24; // Понедельник 24-й недели
struct T.t0 b = {15}; // Инициализация специализации базового типа
b.t0 = 10; // Изменение специализации базового типа
pc1->circle.r = 10; // Изменение радиуса в специализации круга
В случаях, когда подтип обобщения является структурой, обращение к к его полям осуществляется с указанием имени поля, перед которым ставится точка. Обращение к структурной части специализированной переменной осуществляется также как и к обычной структурной переменной.
2.6. Обобщающие параметрические функции
Обобщающие функции обеспечивают поддержку процедурно-параметрического полиморфизма. Их сигнатуры определяют единый интерфейс для соответствующих обработчиков параметрических специализаций. Каждая обобщающая функция имеет от одного до нескольких обобщенных формальных параметров. Они задаются в виде указателей на обобщения. При вызове такой функции в нее передаются ссылки на специализированные переменные, комбинации которых и определяют конкретный обработчик специализации. Предварительное описание обобщающей функции задается ее прототипом:
ПрототипОбобщающейФункции = ТипВозвращаемогоПараметра ИмяФункции
"<" СписокОбобщенныхПараметров ">"
"(" [ СписокФормальныхПараметров ] ")" ["=" "0"] ";"
СписокОбобщенныхПараметров =
ОбобщенныйПараметр {"," ОбобщенныйПараметр }.
ОбобщенныйПараметр = ТипОбобщения "*" ИмяОбобщенногоПараметра.
Прототип обобщающей функции задает обработчик по умолчанию, используемых для комбинаций специализированных параметров, не имеющих обработчиков специализаций. Вместе с тем прототип может задавать отсутствие обработчика по умолчанию, когда используется опция "`= 0`". В этом случае в программе должны быть представлены все обработчики специализаций для функций, определяемы таким (пустым) прототипом.
Обобщающая параметрическая функция, определяющая обработчик по умолчанию, описывается следующими синтаксическими правилами:
ОбобщающаяФункция = ТипВозвращаемогоПараметра ИмяФункции
"<" СписокОбобщенныхПараметров ">"
"(" [ СписокФормальныхПараметров ] ")"
ТелоОбобщающейФункции.
СписокОбобщенныхПараметров = ОбобщенныйПараметр {"," ОбобщенныйПараметр }.
ТелоОбобщающейФункции = ОбработчикПоУмолчанию.
ОбобщенныйПараметр = ТипОбобщения "*" ИмяОбобщенногоПараметра.
Примечание. Учитывая, что шаблонов в Си нет, предлагается использовать угловые скобки. Визуально они неплохо выделяют полиморфные параметры.
В качестве примера рассмотрим пустой прототип обобщающей функции вывода на печать геометрической фигуры. Она может быть представлена следующим образом:
void PrintFigure< struct Figure *f>() = 0;
В этом случае необходимо написать обработчики для всех специализаций. Прототипом функции может передаваться через заголовочный файл для всех конкретных фигур, связанных с соответствующим параметрическим обобщением. Вместо пустой прототипа функции может быть задан обработчик по умолчанию, который вызывается в тех случаях, когда для подставляемой комбинации специализаций не будет реализован свой обработчик. Простейшим вариантов подобного обработчика в таком случае может служить вызов функции прерывания программы:
void PrintFigure<struct Figure *f>() {
printf("Incorrect Argument!!!\n");
exit(-1);
}
Предполагается, что перед вызовами обобщающих функций компилятором будет автоматически генерироваться код, проверяющий наличие всех указателей на обработчики специализаций или, при их отсутствии, на заменяющий их обработчик по умолчанию.
2.7. Обработчики параметрических специализаций
Обработчики параметрических специализаций (или специализированные функции) вызываются при обращении к обобщающей функции. Они обеспечивают непосредственную манипуляцию конкретными специализациями, включенными в состав параметрических обобщений. Синтаксис обработчиков:
СпециализированнаяФункция =
ТипВозвращаемогоПараметра ИмяФункции
"<" СписокСпециализаций ">" "(" СписокФормальных параметров ")"
ТелоСпециализированнойФункции.
СписокСпециализаций =
СпециализированныйПараметр {"," СпециализированныйПараметр }.
СпециализированныйПараметр =
ТипСпециализации "." (ИмяПодтипаСпециализации | "void").
Выбор обработчика осуществляется в зависимости от значений параметрических аргументов, подставляемых вместо формальных параметров. Они могут располагаться в разных единицах компиляции, добавляясь, например, по мере создания очередной специализации, и для обобщающей функции печати фигуры выглядеть следующим образом:
// Вывод параметров прямоугольника
void PrintFigure<struct Figure.rectangle *r>() {
printf("Rectangle: x = %d, y = %d", r->rectangle.x, r->rectangle.y);
}
// Вывод параметров треугольника
void PrintFigure<struct Figure.triangle *t>() {
printf("Triangle: a = %d, b = %d, c = %d",
(*t).triangle.a, (*t).triangle.b, (*t).triangle.c);
}
// Вывод параметров круга
void PrintFigure<struct Figure.circle *r>() {
printf("Circle: r = %d", c->circle.r);
}
Специализированный обработчик для обобщения
Обобщающая функция формируется таким образом, что может использоваться в качестве обработчика по умолчанию для всех специализаций, не имеющих собственных обработчиков. Вместе с тем возможны ситуации, когда нулевая специализация также может нести собственную семантическую нагрузку. При этом возможны ситуации, когда целесообразно сохранить и общий обработчик по умолчанию.
Поэтоиму, наряду с обработчиком по умолчанию, подменяющим все отсутствующие обработчики специализаций, для обобщение возможна реализация собственного уникального обработчика. В этом случае в качестве подтипа обобщенного формального параметра указывается ключевое слово void. Таким образом и на обобщенную переменную можно нагружать свои уникальные функции. Например:
void PrintFigure<struct Figure.void *f>() {
printf("This is a unique operation!!!\n");
exit(-1);
}
Использование нескольких обобщенных аргументов
Основное достоинство процедурно-параметрической парадигмы проявляется при эволюционном расширении в случае множественного полиморфизма, определяющего реализацию мультиметодов. Предлагаемое расширения языка C позволяет безболезненно расширять мультиметоды, добавляя в них новые специализации независимо от числа обобщенных аргументов. В качестве примера можно рассмотреть мультиметод, проверяющий возможность размещения фигуры, задаваемой первым аргументов внутри фигуры, задаваемой вторым аргументом. Пусть изначально для обобщения Figure2, описанного выше, имеется информация только о специализациях прямоугольника и треугольника, заданных с использованием признаков и описанием typedef. В этом случае обобщающая функция может иметь следующий вид:
// Пустой прототип, требующий обязательного определения
// мультиметода для всех специализаций
_Bool Multimethod<Figure2 *first, Figure2 *second>() = 0;
В одной или нескольких единицах компиляции можно сформировать четыре функции, осуществляющие обработку различных комбинаций специализаций. Каждый обработчик специализации описывает одну комбинацию параметров и определяет метод ее обработки:
// Прямоугольник разместится внутри прямоугольника
_Bool Multimethod<Figure2.rect *r1, Figure2.rect *r2>() {
return ((r1->rect.x < r2->rect.x) && (r1->rect.y < r2->rect.y)) ||
((r1->rect.x < r2->rect.y) && (r1->rect.y < r2->rect.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->rect.x*r1->rect.x + r1->rect.y*r1->rect.y)
< (c2->circ.r*c2->circ.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->circ.r < c2->circ.r;
}
К особенностям функций, поддерживающих множественный полиморфизм, можно отнести неопределенность вариантов решений, связанных с комбинациями параметров в ситуациях, когда в обработчиках специализаций могут встречаться как обобщения (нулевые специализации), так и конкретные специализации. Это может формировать разные срезы, которые необходимо описывать отдельно. Для преодоления проблем с такими ситуациями принято решение при котором допускается только обработчик по умолчанию со всеми обобщенными параметрами. Он перекрывает все неописанные обработчики специализаций многомерной параметрической таблицы. Все обработчики специализаций должны в качестве формальных параметров иметь только конкретные специализации. Помимо этого можно использовать в нулевой специализации одну из обрабатываемых основ, применив в определении обобщения опцию default.
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>();
2.9. Дополнительные варианты определения обобщений и специализаций
Базовый вариант определения обобщений и специализаций покрывает практически все возможные ситуации. При использовании специальных функций он позволяет реализовывать всевозможные решения. Однако для повышения эффективности и надежности программирования в языке программирования целесообразно иметь дополнительные варианты, которые можно охарактеризовать как "синтаксический и семантический сахар". Они обеспечивают более наглядное и надежное представление обобщений, а также операций с ними для ряда специфических конструкций. Вполне возможно, что список подобных решений может со временем расширяться.
Определение эволюционно расширяемого перечислимого типа
В том случае, когда в качестве специализаций необходимо использовать только признаки и/или поля общей структуры определение обобщений можно свести к аналогу алгебраических типов данных (АТД). В этой ситуации вместо типа void можно воспользоваться следующим определением обобщения.
ОпределениеАТД = "struct" Имя "{" [СписокЭлементовСтруктуры] "}"
"enum" "{" [СписокПризнаков] "}")
["const"].
СписокПризнаков = Признак {"," Признак}.
Для эволюционно расширения этого аналога перечислимого типа достаточно в этом случае указывать только признаки, разделяемые запятыми.
ДобавлениеПеречислений =
ИмяОбобщения "+" "{" СписокПризнаков "}".
Следует отметить, что для данной конструкции всегда используется нулевая специализация по умолчанию. Это позволяет расширить возможности присваивания друг другу, что объясняется их одинаковым размером из-за отсутствия полей данных у специализаций, в которых задается только признак. Если при базовом определении обобщения допустимо присваивание только между одинаковыми специализациями, то в данном случае возможны следующие дополнительные варианты:
- присваивание нулевой специализации любой другой специализаци;
- присваивание нулевой специализации другой нулевой специализации;
- присваивание нулевой специализации только признака.
То есть, нулевая специализация может служить полиморфным транспортом данных по значению. Присваиваемый признак специализации является конструкцией состоящий из имени обобщения, отделенного точкой от подтипа специализации.
Использование перечислимого типа можно рассмотреть на примере дней недели. В общей структуре при этом содержится число, задающее номер недели в году. Описание дней недели зафиксировано в следующем обобщении и не может более расширяться.
// Дни недели
typedf struct WeekDay {
int week_number;
} enum {
Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday
} const WeekDay;
Ниже предствленны специализированные переменные, задающие базовые значения. Они сохраняются в массиве, состоящем из нулевых специализаций, допускающих присваивание специализаций с любым подтипом
// Объявление переменной для текущей даты.
WeekDay zero = {0};
WeekDay.Monday monday = {1};
WeekDay.Tuesday tuesday = {2};
WeekDay.Wednesday wednesday = {3};
WeekDay.Thursday thursday = {3};
WeekDay.Friday friday = {5};
WeekDay.Saturday saturday = {6};
WeekDay.Sunday sunday = {7};
WeekDay days[10] = { zero, monday, tuesday, wednesday,
thursday, friday, saturday, sunday,
WeekDay.Friday // использование признака специализации
};
days[9] = WeekDay.Saturday;
Полиморфные функции вывода ничем не отличаются от функций, использующи базовое определение.
// Печать дня недели
void printWeekDay<WeekDay* w>() {
printf("Unknown Week Day {%d}\n", w->week_number);
}
void printWeekDay<WeekDay.Monday* w>() {
printf("It is Monday {%d}\n", w->week_number);
}
void printWeekDay<WeekDay.Tuesday* w>() {
printf("It is Tuesday {%d}\n", w->week_number);
}
void printWeekDay<WeekDay.Wednesday* w>() {
printf("It is Wednesday {%d}\n", w->week_number);
}
void printWeekDay<WeekDay.Thursday* w>() {
printf("It is Thursday {%d}\n", w->week_number);
}
void printWeekDay<WeekDay.Friday* w>() {
printf("It is Friday {%d}\n", w->week_number);
}
void printWeekDay<WeekDay.Saturday* w>() {
printf("It is Saturday {%d}\n", w->week_number);
}
void printWeekDay<WeekDay.Sunday* w>() {
printf("It is Sunday {%d}\n", w->week_number);
}
Их использование реализовано в следующем цикле.
for(int i = 0; i < 10; ++i) {
printf("%d. ", i);
printWeekDay<(days+i)>();
}
Определение расширяемого списка указателей
Указатели могут быть разного типа. Однако основы специализаций, связанные с указателями, тоже имеют одинаковый размер. Поэтому для них тоже возможно сформировать специальный вариант определения обобщений. Для него также можно оставить скрытую нулевую специализацию, к которой необходимо добавить дополнительное альтернативное поле, содержащее скрытый указатель. Такая модификация позволяет формировать присваивания аналогично тому, как это реализовано для эволюционно расширяемого перечислимого типа. Альтернативная часть обобщения в этом случае начинается с ключевого слова nullptr.
ОбобщениеУказателей = "struct" Имя "{" [СписокЭлементовСтруктуры] "}"
"nullptr" "{" [СписокПодтиповУказателей] "}"
["const"].
СписокПодтиповУказателей = ПодтипыУказателей ";" { ПодтипыУказателей ";" }.
ПодтипыУказателей = Тип "*" Признак {"," * Признак }.
Возможные варианты присваивания расширяются за счет использования в качестве хранилища нулевой специализации. Реализовано:
- присваивание нулевой специализации любой другой специализаци;
- присваивание нулевой специализации другой нулевой специализации.
В примере демонстрируется использование основ специализаций в обобщенной фигуре через указатели.
typedef struct Figure {} nullptr {Rectangle* rect;} Figure;
Figure + {Triangle* trian;};
void Print<Figure* f>() {
printf("No figure\n");
}
void Print<Figure.rect* f>() {
printf("Rectangle (%d, %d)\n", f->rect->x, f->rect->y);
}
void Print<Figure.trian* f>() {
printf("Triangle (%d, %d, %d)\n", f->trian->a, f->trian->b, f->trian->c);
}
Как и в случае с эволюционно расширяемым перечислимым типом это позволяет создавать массив обобщенных фигур и присваивать его нулевым специализациям значения других специализаций.
int main() {
Rectangle r1 = {3,10};
Rectangle r2 = {13,10};
Triangle t1 = {3,4,5};
Triangle t2 = {30,40,50};
Figure.rect fr1 = &r1;
Figure.rect fr2 = &r2;
Figure.trian ft1 = &t1;
Figure.trian ft2 = &t2;
Figure f1 = ft2;
Figure figures[10] = {
fr1,
fr2,
ft1,
ft2,
f1,
};
figures[5] = figures[0];
figures[6] = figures[1];
figures[7] = figures[2];
figures[8] = figures[3];
for(int i = 0; i < 10; i++) {
Print<(figures+i)>();
}
return 0;
}
Общий синтаксис определения обобщений
В результе добавления дополнительны вариантов определения обобщений окончательный синтаксис выглядит следующим образом.
Обобщение = "struct" Имя "{" [СписокЭлементовСтруктуры] "}"
(
"case" "{" ["default" Подтип ";"] [СписокПодтипов] "}"
| "enum" "{" [СписокПризнаков] "}"
| "nullptr" "{" [СписокПодтиповУказателей] "}"
) ["const"].
3. Дополнительная поддержка с использованием специальных функций
Основная идея, связанная с расширением языка C механизмом поддержки процедурно-параметрического полиморфизма заключается в минимальным влиянии на исходный язык. То есть предполагается отсутствие каких-либо механизмов, изменяющих семантику языка программирования C. Однакое для реализации процедурно-параметрической семантики во время выполнения программы необходима ее дополнительная поддержка, которая реализована за счет введения специальных функций. Представленные ниже функции манипулируют именами типов по аналогии с функцией sizeof. Поэтому относятся к языковым конструкциям, разбираемым во время компиляции. Однако используются они во время выполнения программы.
3.1. Инициализация областей памяти процедурно-параметрическими типами
Явное создание глобальных, статических и локальных переменных осуществляется во время компиляции и средой выполнения автоматически. Основной операцией при этом является установка признака специализации. Однако язык C допускает использование для хранения данных неименованных областей, которые могут быть приведены к желаемому виду путем явного назначения типа. Несмотря на ненадежность подобной операции, имеет смысл ее реализации для специализированных переменных. Функция инициализации специализаций, имеет следующий формат:
ИнициализацияСпециализации =
void 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);
При наличии второго аргумента create_spec создает последовательность специализаций, число которых задается целочисленным выражением, вычисляемым ровно один раз во время выполнения. Например:
int n = read_count();
Figure2.rect *rects = create_spec(Figure2.rect, n);
Значение n == 0 допустимо, при этом функция возвращает нулевой указатель. Отрицательное значение приводит к диагностике времени выполнения. Перед выделением памяти проверяется переполнение размера.
3.3. Функция для определения числа специализаций
В отличие от других механизмов поддержки динамического полиморфизма процедурно-параметрический полиморфизм позволяет получить во время выполнения информацию о количестве специализаций обобщения. Это возможно вследствие локальной идентификации подтипов обобщения числами от 0 до N, задающих параметрические индексы обобщения и его специализаций. Таким образом, общее количество альтернатив, которые могут выступать в роли специализаций, может изменяться от 1 до N, а общее число альтернатив, которые могут использоваться в вычислениях, включая само обобщение, равно N+1.
Это дает возможность реализовать ряд нестандартных подходов к программированию с использованием индексов специализаций. В частности, во время выполнения программы можно получить количество специализаций, воспользовавшись функцией, возвращающей их число, включая и само обобщение, которое в ряде случаев тоже может использоваться в вычислениях:
ЧислоСпециализаций =
int get_spec_size "(" ИмяОбобщения ")".
3.4. Получение указателя на специализацию по параметрическому индексу
Зная количество специализаций, можно использовать их индексы без боязни выйти за максимальное число. Например, можно решить обратную задачу: по индексу специализации получить указатель на соответствующую специализированную переменную. Для этого можно воспользоваться специальной функцией возвращающей указатель на специализацию через указатель на обобщение:
ПолучениеУказателяНаСпециализацию =
УказательНаОбобщение get_spec_ptr
"(" ИмяОбобщения, ИндексСпециализации ")"
Эти константные указатели на фиктивные специализации могут формироваться во время компиляции и не использоваться в ходе вычислений. Их основной задачей является использование в качестве параметрических аргументов для выбора соответствующих обработчиков специализаций, в которых их значение при этом не используется.
3.5. Получение параметрического индекса по типу специализации
После запуска программы фактический параметрический индекс конкретной специализации можно получить по ее типу:
ИндексТипаСпециализации =
int get_subtype_index "(" ИмяОбобщения ["." ПризнакСпециализации] ")".
Само обобщение имеет индекс 0. Если в `case` задана именованная специализация `default`, ее тип также соответствует индексу 0. Остальные специализации получают индексы при запуске программы, поэтому для них нельзя полагаться на номер, предполагаемый из порядка исходных описаний. Функция возвращает фактически назначенный runtime-индекс. Аргументом может быть только обобщение или одна его непосредственная специализация. Цепочки из нескольких признаков этой функцией не принимаются.
3.6. Получение параметрического индекса по специализированной переменной
Индекс фактической специализации объекта можно получить по указателю на него:
ИндексПеременнойСпециализации =
int get_spec_var_index "(" УказательНаСпециализированнуюПеременную ")".
Статический тип указателя может быть типом обобщения или одной из его специализаций. Возвращается индекс, записанный в служебном признаке фактического объекта. Выражение указателя вычисляется ровно один раз. Поэтому, например, сравнение
get_spec_var_index(p) == get_subtype_index(Figure.rect)
проверяет, содержит ли объект, доступный через обобщенный указатель p, специализацию Figure.rect, не зная ее числового индекса заранее.
3.7. Сравнение на равенство двух альтернатив одного обобщения
Использование параметрических индексов позволяет легко реализовать сравнение двух альтернатив, принадлежащих одному обобщению, на равенство специализаций. Достаточно, чтобы индексы сравниваемых переменных совпадали. Для этого используется функция, имеющая следующий формат:
ИдентичностьСпециализаций =
int spec_index_cmp
"(" УказательНаОбобщение, УказательНаОбобщение ")"
Оба указателя должны иметь один и тот же тип обобщения, что легко определяется во время компиляции.
В результате сравнения возвращается целое число, которое может принимать следующие значения:
-
При совпадении специализаций возвращается значение от 0 до N, где N - количество специализаций, включая само обобщение. Возвращаемое значение является индексом проверяемой специализации.
-
Если специализации не совпадают, то возвращается значение, равное -1.
Помимо сравнение на совпадение двух неизвестных специализаций данная функция может использоваться в ситуациях, когда одна из специализаций выступает в роли известного образца. Это позволяет осуществить явную проверку подтипа обобщения во время выполнения программы.
Заключение
Предлагаемые изменения языка программирования PPC направлены на повышение юзабилити и большую совместимость с языком программирования C. Реализация расширенией осуществляется путем их интеграции как в текущий компилятор ppchibicc, так и в компилятор ppclang, разрабатываемый на базе clang [3]. Проект ppclang реализуется под открытой лицензией и размещен на gitverse [4]. Проект ppcchibicc реализуется также под открытой лицензией и размещен на gitverse [5]. Примеры процедурно-параметрических программ также размещены на Гитхаб [6].
Список используемых источников
Легалов А.И., Косов П.В. Расширение языка 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.
Размещение проекта компилятора ppclang с процедурно–параметрической версией языка программирования C на gitverse.
Размещение проекта компилятора ppchibicc с процедурно–параметрической версией языка программирования C на github.
Примеры процедурно-параметрических программ и их сравнение с программами, написанными с использованием других парадигм программирования.
|