|
© 2025
Легалов А.И.
Примеры программ (15.4 кб)
Полиморфизм на простом примере
Понятие динамического полиморфизма достаточно часто встречается в программировании. Оно связано с проверкой альтернативных данных во время выполнения программы. Но не всегда это понятие воспринимается однозначно. С одной стороны любой из поисковиков мог бы предоставить соответствующую информацию. Однако в российской Википедии этого термина не оказалось. На момент написания данного текста полиморфизм в ней застрял в 1967 году. Мне бы не хотелось воспроизводить существующие варианты определений, так как они часто оперируют только ОО полиморфизмом, хотя в настоящее время вариантов реализации динамического полиморфизма гораздо больше. Да и новые могут появиться. Этот материал вряд ли обладает научностью, необходимой для журнальных статей. Скорее всего он отражает плавное течение мыслей, которые на своем персональном сайте я размещаю в заметках о парадигмах программирования.
До разбора ряда вариантов динамического полиморфизма попробую донести свое видение и последующую трактовку на простом примере. Предположим, что Вам предстоит допрос у хорошего или плохого полицейского. Исходя из этого процесс может пойти по различным сценариями. Пусть они выглядят следующим тривиальным образом.
Сценарий общения с плохим полицейским:
printf("Бац-бац...\n");
Сценарий общения с хорошим полицейским:
printf("Именем закона...\n");
При некорректной ситуации (что-то пошло не так) формируется сообщение:
printf("Непонятно, какой полицейский.\n");
Наличие вариантов ведет к проблеме выбора в некоторый момент времени. И именно момент времени определяет каким образом и какое решение будет реализовано.
Статический полиморфизм
Предположим, что нам заранее, с вероятностью 100%, сообщили, каким из полицейских будет проводиться допрос. В результате общая настройка и предваритальная подготовка позволяет отбросить ненужные варианты, оставив только тот, который необходим. В программировании подобный выбор зачастую решается во время компиляции, порождая на выходе готовое решение и отбрасывая ненужные. Обычно оно связано с понятием статического полиморфизма. Например, в C++ одним из вариантов является параметрический полиморфизм, когда использование шаблонов определяет такую технику кодирования, как обобщенное (генеративное) программирование. То есть, до запуска вычислительных процессов условие, определяющее выбор альтернативы оказалось уже известным и разрешенным. Это ведет к порождению конкретных безальтернативных объектов в соответствии с установленными до компиляции параметрами шаблонов. Вот простой пример, когда перед компиляцией определяется функция с тем полицейским, к которому предстоит идти на допрос.
// ОО версия программы про хорошего и плохого полицейских,
// использующая ОО (динамический) полиморфизм
#include <stdio.h>
struct BadCop {
void Action() {printf("Бац-бац...\n");}
};
struct GoodCop {
void Action() { printf("Именем закона...\n"); }
};
struct UnknownCop {
void Action() { printf("Непонятно, какой полицейский.\n"); }
};
template<typename T = UnknownCop> void Action() {
T cop;
cop.Action();
}
int main() {
Action<BadCop>();
// Action<GoodCop>();
// Action<UnknownCop>();
// Action();
return 0;
}
Выбор одного из вариантов связан с раскрытием комментариев одной из строк в функции main, после чего компилятор обработает шаблон, заменив его на соответствующий код.
Вариантов статического полиморфизма много. И их тоже можно отыскать в сети. Но иногда аналогичный выбор можно сделать и без полиморфизма, связав его с заданием значений некоторых переменных еще до компиляции программы. Например, используя препроцессор. В частности допрос можно представить вариантом, напрямую даже не связанным с компилятором языка C. Проверку условия препроцессором вряд ли можно назвать полиморфизмом. Но используя ее, можно избавиться от изменения кода программы за счет "динамической" проверки альтернативы (признака полицейского) во время выполнения препроцессинга.
#include <stdio.h>
#ifdef BAD_COP
// Действие плохого полицейского
void ActionOfBadCop() {
printf("Бац-бац...\n");
}
#elifdef GOOD_COP
// Действие хорошего полицейского
void ActionOfGoogCop() {
printf("Именем закона...\n");
}
#endif
// Обобщенное действие полицейского
void Action() {
#ifdef BAD_COP
ActionOfBadCop();
#elifdef GOOD_COP
ActionOfGoogCop();
#else
printf("Непонятно, какой полицейский.\n");
#endif
}
int main() {
Action();
return 0;
}
При компиляции такого кода достаточно определить препроцессорную переменную, задающую выбор плохого (BAD_COP) или хорошего (GOOD_COP) полицейского. Но это все находится вне содержания последующего изложения.
Динамическая проверка альтернатив во время выполнения
Гораздо чаще возникают ситуации, когда окончательный выбор альтернативы выполняется уже в ходе протекания процессов. То есть, на допрос уже вызвали, и мы стоим перед дверью кабинета (программа откомпилирована и запущена). Стоя перед ней, мы не знаем, какой полицейский нас встретит, но можем прочитать табличку на двери, в которую входим, что позволяет понять, с каким типом (не данных, а полицейского) будем иметь дело.
В этом случае одним из вариантов выбора поведения является проверка типа альтернативы во время выполнения. Во многих (но не во всех) процедурных статически типизированных языках и бестиповых языках (например, в ассемблерах) отсутствует прямая проверка типа во время выполнения. Поэтому обычно используется дополнительный признак, значение которого сопоставляется с каждой альтернативой. В программе на языке C это может реализовано следующим образом.
#include <stdio.h>
// Обобщенный полицейский
typedef enum Cop {
BadCop, // Плохой полицейский
GoodCop, // Хороший полицейский
OtherCop // Иной полицейский
} Cop;
// Действие плохого полицейского
void ActionOfBadCop() {
printf("Бац-бац...\n");
}
// Действие хорошего полицейского
void ActionOfGoogCop() {
printf("Именем закона...\n");
}
// Обобщенное действие полицейского
void Action(Cop cop) {
switch(cop) {
case BadCop:
ActionOfBadCop();
break;
case GoodCop:
ActionOfGoogCop();
break;
default:
printf("Непонятно, какой полицейский.\n");
}
}
int main() {
int cop;
printf("Какой полицейский? (0 - плохой, 1 - хороший): ");
scanf("%d", &cop);
Action(cop==0?BadCop:(cop==1?GoodCop:OtherCop));
return 0;
}
Одним из недостатков явной проверки типов является необходимость знания альтернатив в точке проверки и централизация их анализа. Это ведет к тому, что при расширении программы, приходится модифицировать все аналогичные функции, отвечающие за обработку вариантов. Во многом именно из-за этого процедурный стиль при разработке больших приложений стал менее популярным, и был потеснен объектно-ориентированной парадигмой. Именно ОО подход стал первым, использующим инструментальную поддержку динамического полиморфизма.
Динамический полиморфизм
Суть допроса с применением динамического полиморфизма можно определить следующим образом. Мы не знаем какой из полицейских нас будет допрашивать. И мы не можем проверить этого, так как таблички на двери нет. Просто мы входим в заданный кабинет и огребаем выполнение того сценария, который на нас упадет. Технически отсутствие явной проверки альтернативы подменяется на косвенное связывание программного объекта с некоторой точкой входа (дверью), к которой можно подключать различные альтернативы (комнаты). При этом используется общеизвестный принцип подстановки. Один и тот же процесс допроса в каждом подключаемом варианте ведет к различным сценариям.
На языке программирования C++ это можно представить следующим образом.
#include <stdio.h>
struct Cop { // Обобщенный полицейский
virtual void Action() {
printf("Непонятно, какой полицейский.\n");
}
};
struct BadCop: Cop { // Плохой полицейский
virtual void Action() {
printf("Бац-бац...\n");
}
};
struct GoodCop: Cop { // Хороший полицейский
virtual void Action() {
printf("Именем закона...\n");
}
};
int main() {
int cop;
printf("Какой полицейский? (0 - плохой, 1 - хороший): ");
scanf("%d", &cop);
// Пусть существуют одновременно разные полицейские
// Лень использовать кучу
BadCop badCop; // плохой
GoodCop goodCop; // хороший
Cop otherCop; // иной
Cop* pCop = (cop==0? &badCop:(cop==1? &goodCop: &otherCop));
pCop->Action();
return 0;
}
Основным достоинством такого решения является возможность добавления новых полицейских без изменения обработчиков альтернатив, что ведет к гибкому наращиванию функциональности по сравнению с исходным процедурным подходом.
Однако прогресс не стоял на месте, что привело к внедрению динамического полиморфизма и в процедурные языки. Он реализован в языках Go (интерфейсы) и Rust (типажи). Используемые в них решения, опирующиеся на статическую утиную типизацию, предлагают при написании программ технику во многом аналогичную той, что применяется в ООП. Однако, естественно, есть и свои нюансы. Я не вижу особого смысла приводить сценарии для полицейских на этих языках. Это легко, но тоже не лежит в сфере моего последующего изложения.
Но есть еще один процедурный вариант динамического полиморфизма, который мне близок. Он используется в процедурно-параметрической парадигме программирования (4П). Предлагаемое решение также расширяет возможности чистого процедурного подхода независимо от того, применяется императивное или функциональное программирование. Разработанные технические решения могут быть реализованы не только в новых языках, но и быть добавлены в уже существующие, что мы и сделали с языком программирования C. Создаваемые, на процедурно-параметрическом C (PPC) программы, обеспечивают гибкое эволюционное расширение как альтернативных данных, так и функций, обеспечивающих их обработку [PPCЛегалов А.И., Косов П.В. Процедурно-параметрическое расширение языка программирования C. Синтаксис и семантика]. Об этом уже много написано и есть открытый репозиторий с различными демонстрационными программами. Процедурно-параметрическая (ПП) программа, о плохом и хорошем полицейскими имеет следующий вид.
#include <stdio.h>
// Обобщенный полицейский
typedef struct Cop {}<> Cop;
// Плохой полицейский
Cop + <Bad: void;>;
// Хороший полицейский
Cop + <Good: void;>;
// Обобщенное действие полицейского
void Action<Cop* cop>() {
printf("Непонятно, какой полицейский.\n");
}
// Действие плохого полицейского
void Action<Cop.Bad* cop>() {
printf("Бац-бац...\n");
}
// Действие хорошего полицейского
void Action<Cop.Good* cop>() {
printf("Именем закона...\n");
}
int main() {
int cop;
printf("Какой полицейский? (0 - плохой, 1 - хороший): ");
scanf("%d", &cop);
// Пусть существуют одновременно разные полицейские
// Лень использовать кучу
struct Cop.Bad badCop; // плохой
struct Cop.Good goodCop; // хороший
Cop otherCop; // иной
Action<cop==0? (Cop*)&badCop:(cop==1? (Cop*)&goodCop: &otherCop)>();
return 0;
}
Надеюсь, что этот предварительный экскурс раскрывает мое понимание и трактовку динамического полиморфизма: выбор и обработка альтернатив во время выполнения программы без явной проверки их типа.
ООП и ОО полиморфизм
Существуют различные подходы к разработке программного обеспечения. Некоторые настоящее время являются доминирующими. Их концепции легли в основу популярных языков программирования. И особенно среди них выделяется объектно-ориентированная (ОО) парадигма. Ключевыми при этом считаются идеи Алана Кея, который позиционируется как основоположник ООП.
Что говорил Алан Кей
В одном из интервью (2003 год) Алан Кей рассказал о своем понимании ООП.
Выборочно цитирую из [AKay2003enDr. Alan Kay on the Meaning of “Object-Oriented Programming, AKay2003ruДоктор Алан Кей о смысле «объектно-ориентированного программирования].
В Юте, где-то после ноября 1966 года — под влиянием системы Sketchpad, языка Simula, проекта ARPAnet, компьютера Burroughs B5000 и моего опыта в биологии и математике — у меня сформировался замысел определенной архитектуры для программирования. Вероятно, уже в 1967 году кто-то спросил меня, над чем я работаю, и я ответил: «Это объектно-ориентированное программирование».
Изначальная концепция включала следующие положения:
Я представлял объекты как биологические клетки и/или отдельные компьютеры в сети, способные общаться только при помощи сообщений...
Я хотел уйти от данных... Я осознал, что метафора клетки/отдельного компьютера позволяет исключить данные...
... Термин «полиморфизм» был введен гораздо позже ... и не вполне корректен...
Меня не устраивала реализация наследования в Simula I и Simula 67... Поэтому я решил исключить наследование из встроенных возможностей языка, пока не разберусь в нем глубже...
Первый Smalltalk в Xerox PARC вырос именно из этих идей.
Я не против типов, но я не знаю ни одной системы типов, которая не была бы сплошной головной болью, поэтому я до сих пор предпочитаю динамическую типизацию.
Для меня ООП — это только отправка сообщений, локальное хранение, защита и сокрытие состояния-процесса, а также предельно позднее связывание всего.
Конец выборочного предвзятого цитирования.
Если опираться на эти рассуждения, то можно прийти к выводу, что Алан Кей говорил не столько о технике написания кода, сколько об общем дизайне программы, который в процедурном программировании обычно рассматривается на уровне модулей. Речь не шла ни о данных, ни о том, как должны быть реализованы алгоритмы внутри объектов. Только о взаимодействии объектов посредством сообщений, играющих роль интерфейсов между ними. Это практически все равно, если бы мы говорили о бактериях, не вникая в то, как они устроены внутри, лишь наблюдая за их взаимодействием из вне.
Также напрашиваются ассоциации с кибернетикой. Такой "реакционной лженауке современных рабовладельцев" по определению некоторых советских "ученых". Снаружи те же черные ящики с входами и выходами. А что внутри - попробуй угадай. Но не будем о прошлом. В контексте описанной модели интереснее порассуждать о динамическом полиморфизме.
Он присутствует, даже несмотря на то, что объекты могут не иметь между собой родства (наследования). Главное, чтобы у них была возможность получать одинаковые сообщения. И это позволяет взаимодействовать с разными объектами одинаковым образом, подменяя их при необходимости. При этом совсем не обязательно, чтобы интерфейсы данных объектов между собой полностью совпадали, так как другие сообщения мы передавать не собираемся. Пример динамического связывания неродственных объектов можно продемонстрирвать с использованием Питона.
class Cop: # Обобщенный (иной) полицейский
def Action(self):
print("Непонятно, какой полицейский.")
class BadCop: # Плохой полицейский
def Action(self):
print("Бац-бац...");
class GoodCop: # Хороший полицейский
def Action(self):
print("Именем закона...");
#----------------------------------------------
if __name__ == '__main__':
tag = int(input("Какой полицейский? (0 - плохой, 1 - хороший): "))
if tag == 0:
cop = BadCop() # плохой
elif tag == 1:
cop = GoodCop() # хороший
else:
cop = Cop() # иной
cop.Action()
Подобная несвязность объектов может иметь как положительные, так и отрицательные стороны. В частности, можно передать в первый объект другое сообщение, которое не может получать второй рассматриваемый объект. Но можно подменить его на третий объект, который сможет обработать это сообщение, не не сможет принять первое сообщение. И такая ситуация при связывании с любыми объектов может привести к тому, что в конце концов мы подключим объект, который не сможет принимать ни первое, ни второе сообщение. Поэтому наряду с организацией взаимодействия различных объектов для контроля взаимодействий зачастую необходимо использовать явную проверку типа объекта посредством передачи соответствующего сообщения, на которое положительно или отрицательно могут отвечать все объекты. Это ведет к потере производительности, но поддерживает гибкое взаимодействие и переключение объектов различного типа за счет динамической проверки типов во время выполнения. В противном случае все взаимодействующие объекты должны иметь возможность реакции на все сообщения, циркулирующие в общем бактериальном (программном) бульоне, что тоже вряд ли является эффективным решением.
А какие живые организмы живут в процедурном подходе?
На мой взгляд аналогии из биологии не очень-то подходят для определения стилей программирования. Но если Алан Кей сказал "А", по поводу живых клеток, то почему бы не продолжить такое сопоставление, перенеся его на процедурный подход. В частности, независимые от данных функции можно представить как вирусы, которые поедают другие живые и неживые клетки, модифицируя тем самым окружающее их пространство. И если поедаемые живые клетки - это объекты Алана Кея, то можно даже говорить о смешанном (мультипарадигменном) программировании, держа при этом в голове мысль, что сравнение различных стилей программирования с естественным поведением объектов или процессов реального мира - это, все-таки, маразм.
Статика, Страуструп, иное ООП
Несмотря на то, что изначально в основу ОО программирования (ООП) были заложены идеи Алана Кея, в настоящее время предпочтения отдается другим взглядам, которые технически обеспечивают более эффективные решения. Это определяется тем, что языки со статической системой типов в ходе компиляции порождают гораздо более быстрый код, а также предполагают его большую надежность за счет статического анализа и формальной верификации. Сформированный на основе наследования и виртуализации ОО полиморфизм, являющийся одной из разновидностей динамического полиморфизма, позволил повысить гибкость процессов разработки программного обеспечения (ПО). Сочетание статической типизации и ОО подхода используется достаточно широко в создании программных систем различного назначения как в императивном, так и в функциональном программировании.
Современную трактовку понятия ООП можно прочитать, например, в Википедии [OOPВикипедия. Объектно-ориентированное_программирование]. В определенной степени точка зрения на первоначально восприятие поменялась достаточно давно. Меня, в свое время, удовлетворило определение ООП из книги Тимоти Бада [BaddБадд Т. Объектно-ориентированное программирование в действии. /Пер. с англ. - СПб: Питер, 1997 - 464 с.]. Ссылаясь на Алана Кея (который этого не говорил) он провозгласил следующие фундаментальные характеристики:
Все является объектом.
Вычисления осуществляются путем взаимодействия между объектами.
Объект имеет память, состоящую из других объектов.
Объект - представитель класса, выражающего общие свойства объектов.
В классе задается поведение (функциональность) объекта.
Классы организованы в древовидную структуру с общим корнем (иерархия наследования, использующая другой подход к описанию альтернатив).
В целом это хорошо подходит под разработанный Бьёрном Страуструп язык C++.
Что же, на мой взгляд, сделал Бьёрн Страуструп? По всей видимости, он лучше Алана Кея разобрался с наследовованием, избавившись при этом от динамической типизации. Результатом явились создание по сути первого промышленного статически типизированного языка с классами, компилируемого в высокоэффективный код. Разработанный мультипрадигменный язык практически на десятилетие стал эталоном ООП и послужил в дальнейшем прототипом для других более "истинных" подражателей, первым из которых появился Java, а вслед за ним и C# (думаю, что при трезвом рассмотрении их тоже можно отнести к мультипарадигменным языкам).
В чем основная идея, связанная с поддержкой динамического полиморфизма? Она проявляется через совместное использования виртуализации и наследования. Виртуализация при этом обычно реализуется за счет таблиц виртуальных методов (VT). Таблица базового класса Base, выступающего в роли обобщения, содержит указатели на один или несколько методов (F), которые обычно переопределяются в виртуальных таблицах производных классов, являющихся специализациями обобщения (Child1–Childn). При этом каждый производный класс, расширяя базовый класс, имеет собственный тип, что выражается в уникальном имени класса (Childi). Производные классы формируются независимо друг от друга. Это позволяет эволюционно расширять альтернативные программные объекты, каждый из которых, при наличии одинаковых интерфейсов, может иметь иную функциональность, обеспечивая тем самым реализацию ОО полиморфизма. Однако добавление нового виртуального метода требует модификации всей иерархии классов. Также данный механизм напрямую не поддерживает мультиметоды, для реализации которых ОО решением является использование диспетчеризации, не способствующей безболезненному расширению классов.
Стремление к созданию высокоэффективных программ привели к мультипарадигменному единению в C++ вирусов и бактерий. Это конечно же справедливо было воспринято как не ООП [AKey1997Alan Kay at OOPSLA 1997 - The computer revolution hasnt happened yet]. Но нужен ли чистый ОО подход в программировании? По прошествии с тех пор лет оптимизм похоже сдулся и прорывов в истинном ООП вроде бы не произошло.
В своем первоначальном исполнении плюсы действительно были только Си с классами. По крайней мере я достаточно долго в различных проектах обходился без шаблонов, подменив динамическую проверку типов на ОО полиморфизм. Использовать шаблоны мы начали где-то около нулевых, что нашло отражение в первом интерпретаторе языка Пифагор [LKKPЛегалов А.И., Казаков Ф.А., Кузьмин Д.А., Привалихин Д.В. Функциональная модель параллельных вычислений и язык программирования Пифагор" - 2002-2003.]. Естественно, что за годы, прошедшие со времени создания C++, стараниями не только Страуструпа, но и комитета по стандартизации, он дополнительно оброс многими позитивными и негативными наростами, что привело к его отрицательному восприятию программистами-гурманами. Однако это судьба практически любого долго живущего языка, функциональность и мощь которого пытаются улучшить. Изначально Java, С# тоже были мягкими и пушистыми. C каждой новой версией в них также стали появляться разные новообразования. Нет гарантией, что, к примеру, Go, Rust и другие не последуют этим же путем. И как сказал по этому поводу Страуструп: "Есть всего 2 типа языков: те, на которые все жалуются и те, которыми никто не пользуется."
Процедурный подход и динамический полиморфизм
В течение длительного времени процедурный подход находился в застое, что во многом объяснялось отсутствием поддержки динамического полиморфизма. Это не позволяло без дополнительных усилий писать гибко изменяемый и повторно используемый код. Однако в настоящее время ситуация меняется. Появление языков Go и Rust, поддерживающих статическую утиную типизацию, позволяет создавать программы, аналогичные по критериям качества объектно-ориентированным. Это в частности подтверждается возможностью воспроизводства ОО паттернов проектирования, хотя сами языки, на мой взгляд, относятся к процедурным. Возможно, что процедурные подходы в недалеком будущем составять более сильную конкуренцию ООП.
Процедурно-параметрический полиморфизм
Еще большие возможности по гибкости и эволюционному расширению программ за счет иного подхода к реализации динамического полиморфизма предоставляет процедурно-параметрическая парадигма программирования (4П). Лежащее в его основе достаточно простое техническое решение ориентировано на поддержку дальнейшего развития процедурного подхода, изначально разделяющего данные и функции. Обладая более широкими возможностями по сравнению с другими методами поддержки динамического полиморфизма, процедурно-параметрическое программирование (ППП), позволяет гибко создавать эволюционно-расширяемые программы. Вместе с тем данный подход еще не получил широкого распространения, что, на наш взгляд, связано с отсутствием инструментальной и языковой поддержки. Разрабатываемый на основе языка C процедурно-параметрический C (PPC) находится на начальном этапе, хотя уже позволяет демонстрировать многие возможности 4П.
Эти возможности представлены в различных материалах и подтверждаются примерами программ, разработанных на PPC. В частности, обеспечивается более гибкое эволюционное расширение в различных ситуациях, имеется простая и эффективная поддержка множественного полиморфизма (мультиметодов), при которой одиночный полиморфизм (монометоды) является частным случаем, многие проектные решения, например, описанные ОО паттернами проектирования могут быть реализованы более гибко и эффективно [PP-patternsЛегалов А.И. Процедурно-параметрическая парадигма и паттерны ОО проектирования - 2025, OOP-PPPКосов П.В., Легалов А.И. Сравнение объектно-ориентированного и процедурно-параметрического полиморфизма. Труды Института системного программирования РАН. 2025;37(6):43-58.]. Такие принципы, как SOLID, реализуются естественным путем за счет раздельной организации данных и функций.
Возможности ПП подхода обеспечиваются за счет достаточно простой инструментальной поддержки. Обобщение строится из раздельно формируемых основ специализаций, в качестве которых могут выступать различные типы данных. Каждая формируемая при этом специализация имеет признак, представляемый в порождаемом системой программирования выходном представлении целым числом, что позволяет использовать его в качестве индекса от 0 до сформированного числа специализаций.
Процедурно–параметрический полиморфизм обеспечивает формирование альтернативных специализаций (specialization1 – specializationn), используя для этого основы специализаций (foundation1 – foundationn), которые являются подтипами данных. Добавление альтернативных специализаций к обобщающему их типу данных (Generalization) может осуществляться в произвольные моменты времени и в различных единицах компиляции. Формируемые при этом специализации обобщения принадлежат к тому же типу, что и обобщающий тип, являясь его подтипами. Само обобщение также трактуется как одна из специализаций (specialization0), которая содержит только свои внутренние данные. Обработка сформированных альтернатив осуществляется с использованием внешних независимых специальных функций – обработчиков специализаций, расширяющих обобщающую их функцию. Они также могут добавляться независимо друг от друга. Механизм идентификации специализаций локализован внутри каждого обобщения с использованием внутренней индексации (параметрических индексов). Этот же механизм используется для автоматического выбора обработчиков специализаций через параметрические таблицы, каждая из которых связана со своим набором обобщающих функций. Добавление новых обобщающих функций и их обработчиков специализаций может осуществляться в любой момент времени независимо от уже существующих данных и функций, что характерно для процедурного подхода. Эти функции могут содержать произвольное число обобщающих аргументов, обеспечивая прямую реализацию эволюционно расширяемых мультиметодов [MMЛегалов А.И., Косов П.В. Расширение языка C для поддержки процедурно-параметрического полиморфизма. Моделирование и анализ информационных систем. 2023;30(1):40-62.].
В языке программирования признак задается идентификатором. В частных случаях имя признака может совпадать с именем типа, что позволяет его опускать, а в основах специализации могут использоваться неименовнанные типы или пустой тип void.
В целом, характеризуя 4П, можно отметить некоторые из следующих свойств (которых на самом деле значительно больше).
Это иная по сравнению с другими разновидность динамического полиморфизма с непосредственной инструментальной поддержкой мультиметодов.
Поддерживается гибкое эволюционное расширение программ без изменения ранее написанного кода как при нисходящем, так и при восходящем проектировании.
Относительная независимость процедурно-параметрического механизма позволяет использовать данный подход с другими парадигмами и, как следствие, возможность ее встраивания даже в уже существующие языки программирования.
Обобщение можно формировать как до создания основ специализаций (как и в ООП), так и после, обобощая уже существующие основы специализаций.
Можно создавать произвольное число обобщений от одних и тех же основ специализаций.
Существуют дополнительные возможности при при использовании вместо ОО подхода (например, альтернативная реализация паттернов ОО проектирования).
- В качестве специализаций можно использовать различные программные объекты (именованные типы, указатели на них, неименованные структуры).
Возможность гибкой эквивалентной трансформации и оптимизации структуры ПП программ, что позволяет легко заменить параметрические таблицы на другие методы реализации, включая использование конструкций, применяемых в традиционном процедурном (монолитном) программировании.
Если в вызове обработчика специализаций подставлены конкретные специализации, то можно убрать из параметрической таблицы соответствующие измерения. Вплоть до непосредственной подстановки нужного обработчика.
О моделировании динамического полиморфизма
Если в языках программирования отсутствует поддержка динамического полиморфизма, то его можно смоделировать. С одной стороны это позволяет понять особенности технической реализации, другой - обеспечить достижение требуемых критериев качества, которые при прямых решениях могут оказаться нереализуемы.
Моделирование динамического полиморфизма, при отсутствии соответствующей языковой и инструментальной поддержки, используется в языке программирования zig [ZIGKarl Seguin. Learning Zig]. На этой основе реализованы многие библиотеки языка. Следует отметить, что его синтаксис и семантика во многом способствует такому моделированию, позволяя не включать в язык непосредственную реализациию. Возможно, правда, до определенной поры. Как было уже отмечено, языки программирования имеют свойство распухать (или уходить в забвение).
Вполне естественно, что моделирование различных механизмов является также первой стадией отработки того или иного технического решения. При получении успешных результатов отработанный подход используется уже внутри некоторой покрывающей его оболочки, которой для языков программирования являются соответствующие языковые конструкции. Они во время компиляции и, возможно, на последующих этапах трансформации, используют отработанные решения. Изначально подобное происходил и для C с классами, который просто ретранслировал оболочки в код на C, отработаный на уровне моделей. Вариант такой трансляции представлен в книге Голуба [ГолубГолуб А.И. C, C++. Правила программирования. М: Бином. 1996. - 272 с.]. Возможные варианты, демонстрирующие непосредственное использование подобного приема я в свое время описал в материале, представленном на сайте [DHPЛегалов А.И. Разнорукое программирование. Как "прикинуться" объектным обобщением - 2001.]. Подобные модели в различных вариациях выстраивались нами и для 4П [MMЛегалов А.И., Косов П.В. Расширение языка C для поддержки процедурно-параметрического полиморфизма. Моделирование и анализ информационных систем. 2023;30(1):40-62., PPMODELЛегалов А.И. Эволюция мультиметодов при процедурном подходе - 2002.].
Моделирование ретро ООП с применением 4П
Наряду с отработкой новых идей на старых языках бывает интересно посмотреть, во что выливается реализация ранее разработанных систем с применением более свежих подходов. Во многом это скорее для развлечения и удовлетворения любопытства, но все же. Обратимся к варианту ООП, предложенному Аланом Кеем, который в основном ориентирован на динамическую типизацию и определение наличия полиморфных методов во время выполнения за счет эффективных поисковых алгоритмов и хеширования. Это практически всегда медленнее, чем использование прямых табличных обращений, но объекты, имеющие одинаковые методы могут быть между собой не связаны никакими другими отношениям, включая наследование.
Несмотря на проигрыш по производительности, ОО языки с динамической типизацией и без наследования, существуют и живут при этом неплохо. В частности среди них обитает и Python. При обычной интерпретации производительность методов, использующих наследование, в нем сопоставима с произодительностью методов, расположенных в различных несвязанных классах. По всей видимости (хотя я профан в особенностях реализации Питона и могу ошибаться) это связано с тем, что таблицы виртуальных методов строить не имеет смысла, так как эти методы должны вызываться и в случаях, идет обращение к объектам, не входящим в иерархию наследования. Сочетание в системе двух вариантов обращения может оказаться громоздким, хотя для получения требуемой эффективности иногда можно пойти и на это. Но это только мои ничем не подкрепленные соображения.
Вместе с тем, в ряде случаев было бы неплохо применить прямые обращения к методам с одинаковой сигнатурой, расположенных в классах, не связанных наследованием. В принципе такие комбинации могут порождаться при компиляции с ОО языков, использующих динамическую типизацию, в языки со статической типизацией. В качестве примера (для удовлетворения любопытства) ниже рассматривается вариант моделирования несвязанных объектов с применением процедурно-параметрического подхода.
Формирование объектов из основ специализаций
Используется уже избитый мною до синяков пример, когда описания различных фигур из файла загружаются в массив моделируемых объектов, после чего выводятся из него с применением динамического полиморфизма. Предполагается (с некоторыми допущениями), что все есть обобщающий объект с именем O. Различные другие объекты выводятся из O путем формирования соответствующих специализаций. Нельзя конечно сказать, что они совсем не связаны между собой. Однако наличие только общей точки подключения, через которую осуществляется реализация ПП полиморфизма, позволяет порождать любой новый объект независимо от других.
Другим аргументом в пользу модели с общим родительским объектом является то, что не предполагается замена этим решением ОО подхода, использующего полностью независимые объекты. Однако в ходе компиляции вполне возможна трансформация исходной семантической модели в ту, что рассматривается здесь. Подобные трансформации являются обычным явлением при построении компиляторов. По сути все языки программирования, независимо от реализованных в них парадигмах, трансформируются в машинный код условной Фон-Неймановской архитектуры.
Еще одним допущением является то, что модель объекта вместо внутренних методов ориентируется на использование внешних функций, связанных с "объектами" через обобщающий параметр. Однако реализация функций объектов вне структуры данных - это не новое решение. Подобный подход, например, используется в таких языках программирования, как Оберон-2 и Ада-95. Вторым допущением, что имя полиморфной функции стоит перед аргументом, связанным с типом (размещен в угловых скобках), при моделировании также можно пренебречь.
Ниже приведены отдельные фрагменты кода, представленного в репозитории для более детального ознакомления. Исходя из принятых соглашений, будем манипулировать обобщеным объектом со следующим интерефейсом.
//------------------------------------------------------------------------------
// Общий для всех объект. Название выбрано, чтобы короче писать.
typedef struct O {}<> O;
//------------------------------------------------------------------------------
// Прототипы обобщающих функций. Можно добавлять эволюционно, но в данном
// примере это не играет роли.
// Информация о типе объекта
char* TypeInfo<O* o>();
// Обобщенная функция ввода данных из входного потока
void Input<O* o>(FILE* f);
// Обобщенная функция вывода данных в выходной поток
void Print<O* o>(FILE* f);
// Инициализация разных объектов
void Init<O* o>();
// Завершение работы разных объектов
void Сomplete<O* o>();
Обработчики по умолчанию реализуют различные стратегии, обеспечивающие поведение обобщенного объекта, который в принципе тоже может существовать. Например, в качестве заглушки вместо пустого указателя. В качестве примера используются следующие реализации этих обработчиков.
//------------------------------------------------------------------------------
// Информация о типе объекта
char* TypeInfo<O* o>() {
return "Unknown";
}
//------------------------------------------------------------------------------
// Обобщенная функция ввода данных из входного потока
void Input<O* o>(FILE* f) {
fprintf(f, "The object do not have Input message\n");
exit(11);
}
//------------------------------------------------------------------------------
// Обобщенная функция вывода данных в выходной поток
void Print<O* o>(FILE* f) {
fprintf(f, "The object do not have Print message\n");
exit(12);
}
//------------------------------------------------------------------------------
// Инициализация разных объектов
void Init<O* o>() {}
//------------------------------------------------------------------------------
// Завершение разных объектов
void Сomplete<O* o>() {}
Формируемые "объекты" являются специализациями, при необходимости переопределяющими обработчики по умолчанию и добавляющими свои внутренние методы, которые могут быть как методами для всех объектов (если используется ПП полиморфизм), так и их частными методами (реализованными в виде обычных функций).
//------------------------------------------------------------------------------
// Целочисленный объект
O+<int;>;
//------------------------------------------------------------------------------
// Поток как объект. Можно отдельно сделать Istream и Ostream. Без проблем.
// Но не в этой жизни. Хотя безопасность можно повысить.
typedef struct Fstream {
FILE* f;
} Fstream;
O+<Fstream;>;
// Отрытие файла для чтения или записи
void FileOpen(struct O.Fstream* f, char* fileName, const char* opt);
// Закрытие файла, реализованного как объект
void FileClose(struct O.Fstream* f);
// Обобщенная функция ввода данных из входного потока
void Input<O* o>(struct O.Fstream* f);
// Обобщенная функция вывода данных в выходной поток
void Print<O* o>(struct O.Fstream* f);
//------------------------------------------------------------------------------
// прямоугольник, содержащий целочисленные объекты
typedef struct Rectangle {
struct O.int x; // ширина
struct O.int y; // высота
// int x, y; // ширина, высота
} Rectangle;
O+<Rectangle;>;
//------------------------------------------------------------------------------
// треугольник
typedef struct Triangle {
struct O.int a, b, c; // стороны треугольника
} Triangle;
O+<Triangle;>;
//------------------------------------------------------------------------------
// Простейший контейнер на основе одномерного массива
typedef struct Container {
int len; // текущая длина
struct O* cont[max_len];
} Container;
O+<Container;>;
Для каждого "объекта" реализуются обработчики специализаций, а также отдельные функции там, где общность отсутствует.
//==============================================================================
// Описание обработчиков специализаций прямоугольника
//==============================================================================
//------------------------------------------------------------------------------
// Информация о типе объекта
char* TypeInfo<O.Rectangle* o>() {
return "Rectangle";
}
//------------------------------------------------------------------------------
// Обработчик ввода данных для прямоугольника
void Input<O.Rectangle* o>(struct O.Fstream* f) {
Input<(O*)&(o->@x)>(f);
Input<(O*)&(o->@y)>(f);
}
//------------------------------------------------------------------------------
// Обработчик вывода данных прямоугольника
void Print<O.Rectangle* o>(struct O.Fstream* f) {
fprintf(f->@f, "It is Rectangle: x = "); Print<(O*)&(o->@x)>(f);
fprintf(f->@f, ", y = "); Print<(O*)&(o->@y)>(f);
fprintf(f->@f, "\n");
}
//------------------------------------------------------------------------------
// Инициализация прямоугольника
void Init<O.Rectangle* o>() {
init_spec(O.int, &o->@x);
init_spec(O.int, &o->@y);
}
//==============================================================================
// Описание обработчиков специализаций треугольника
//==============================================================================
//------------------------------------------------------------------------------
// Информация о типе объекта
char* TypeInfo<O.Triangle* o>() {
return "Triangle";
}
//------------------------------------------------------------------------------
// Обработчик ввода данных для треугольника
void Input<O.Triangle* o>(struct O.Fstream* f) {
Input<(O*)&(o->@a)>(f);
Input<(O*)&(o->@b)>(f);
Input<(O*)&(o->@c)>(f);
}
//------------------------------------------------------------------------------
// Обработчик вывода данных треугольника
void Print<O.Triangle* o>(struct O.Fstream* f) {
fprintf(f->@f, "It is Triangle: a = "); Print<(O*)&(o->@a)>(f);
fprintf(f->@f, ", b = "); Print<(O*)&(o->@b)>(f);
fprintf(f->@f, ", c = "); Print<(O*)&(o->@c)>(f);
fprintf(f->@f, "\n");
}
//------------------------------------------------------------------------------
// Инициализация треугольника
void Init<O.Triangle* o>() {
init_spec(O.int, &o->@a);
init_spec(O.int, &o->@b);
init_spec(O.int, &o->@c);
}
//==============================================================================
// Описание обработчиков специализаций контейнера
//==============================================================================
//------------------------------------------------------------------------------
// Информация о типе объекта
char* TypeInfo<O.Container* o>() {
return "Container";
}
//------------------------------------------------------------------------------
// Обработчик ввода данных для контейнера
void Input<O.Container* o>(struct O.Fstream* f) {
while(!feof(f->@f)) {
if((o->@cont[o->@len] = FigureCreateAndIn(f)) != 0) {
o->@len++;
}
}
}
//------------------------------------------------------------------------------
// Обработчик вывода данных контейнера
void Print<O.Container* o>(struct O.Fstream* f) {
fprintf(f->@f, "Container contains %d elements.\n", o->@len);
for(int i = 0; i < o->@len; i++) {
fprintf(f->@f, "%d: " , i);
Print<o->@cont[i]>(f);
}
}
//------------------------------------------------------------------------------
// Инициализация контейнера
void Init<O.Container* o>() {
o->@len = 0;
}
//------------------------------------------------------------------------------
// Очистка контейнера
void Сomplete<O.Container* o>() {
for(int i = 0; i < o->@len; i++) {
free(o->@cont[i]);
}
Init<(O*)o>();
}
//==============================================================================
// Описание обработчиков специализаций для целочисленного объекта
//==============================================================================
//------------------------------------------------------------------------------
// Информация о типе объекта
char* TypeInfo<O.int* o>() {
return "int";
}
//------------------------------------------------------------------------------
// Обработчик ввода данных для целого числа
void Input<O.int* o>(struct O.Fstream* f) {
fscanf(f->@f, "%d", &(o->@));
}
//------------------------------------------------------------------------------
// Обработчик вывода данных для целого числа
void Print<O.int* o>(struct O.Fstream* f) {
fprintf(f->@f, "%d", o->@);
}
//------------------------------------------------------------------------------
// Инициализация целого числа
void Init<O.int* o>() {
// init_spec(O.int, o);
o->@ = 0;
}
//==============================================================================
// Описание обработчиков специализаций для файлового потока
//==============================================================================
//------------------------------------------------------------------------------
// Информация о типе объекта
char* TypeInfo<O.Fstream* o>() {
return "Fstream";
}
//------------------------------------------------------------------------------
// Отрытие файла для чтения или записи
void FileOpen(struct O.Fstream* o, char* fileName, const char* opt) {
o->@f = fopen(fileName, opt);
if(o->@f == NULL) {
printf("Incorrect file name = %s\n", fileName);
exit(13);
}
}
//------------------------------------------------------------------------------
// Закрытие файла, реализованного как объект
void FileClose(struct O.Fstream* o) {
fclose(o->@f);
}
Файловый поток не является полиморфным объектом, который имеет общие функции, аналогичные другим объектам. Поэтому большинство из них пустые. Его собственные функции можно сделать полиморфными от объекта, что приведет к формированию разреженных парамететрических таблиц обработчиков специализаций. Поэтому принято решение сделать их автономными, что возможно и для других объектов тоже, если у них есть уникальные функции, не реализуемые другими. В этом случае используется непосредственное обращение к подобным объектам, так в момент использования точно известны их типы.
Работа с данными, моделирующими объекты, мало отличима от использования абстракций обычной ПП программы.
int main(int argc, char* argv[]) {
if(argc !=3) {
printf("incorrect command line!\nWaited: command infile outfile\n");
return 1;
}
// FILE* ifst = fopen(argv[1], "r");
// FILE* ofst = fopen(argv[2], "w");
struct O.Fstream ifst; FileOpen(&ifst, argv[1], "r");
struct O.Fstream ofst; FileOpen(&ofst, argv[2], "w");
struct O.Fstream cout; cout.@f = stdout;
printf("Start\n");
struct O.Container c;
Init<(O*)&c>();
Input<(O*)&c>(&ifst);
FileClose(&ifst);
fprintf(stdout, "Filled container.\n");
Print<(O*)&c>(&cout);
fprintf(ofst.@f, "Filled container.\n");
Print<(O*)&c>(&ofst);
Сomplete<(O*)&c>();
fprintf(stdout, "Empty container.\n");
Print<(O*)&c>(&cout);
fprintf(ofst.@f, "Empty container.\n");
Print<(O*)&c>(&ofst);
FileClose(&ofst);
printf("Stop\n");
return 0;
}
Что-то пошло не так...
Моделируя компиляцию независимых объектов в ПП представление, мы ускоряем доступ к методам, одинаковым по сигнатуре. Однако в общем случае, когда число различных объектов, имеющих не совпадающие по сигнатуре и назначению методы, велико, параметрические таблицы, содержащие ссылки на обработчики специализаций становятся разреженными. Поэтому для подобных ситуаций нужны другие подходы, связанные с известными уже решениями: эффективным поиском, хешированием, просто реализацией через переключатели или условные операторы. Подобные проблемы встают не только в моделировании независимых объектов (там они проявляются явно), но и в ряде ситуаций, когда 4П непосредственно используется при написании программ. Эта проблема еще в большей степени может проявляться, когда в функциях используется множественный полиморфизм. Исходя из этого встает задача оптимизации параметрических отношений за счет сжатия их таблиц с применением соответствующих методов, включая и выше описанные.
Возникающие ситуации и наброски вариантов, обеспечивающих решения, можно рассмотреть на следующем простом примере. Определим в качестве набора специализаций ряд предопределенных типов данных.
// Объект для всего
typedef struct O {} <> O;
O+<int;>; // Целое как объект
O+<double;>; // Действительное как объект
O+<char;>; // Символ как объект
O+<str: char*;>; // Указатель на строку как объект
Существует ряд полиморфных операций, которые общие для всех специализаций. Поэтому для их объединения целесообразно использовать ПП отношения.
//==============================================================================
// Общие полиморфные операции для одиночных объектов
//==============================================================================
//------------------------------------------------------------------------------
// Информация о типе объекта. Обобщенная функция
char* TypeInfo<O* o>() {return "Undefined";}
// Информация о типе целочисленного объекта
char* TypeInfo<O.int* o>() {return "int";}
// Информация о типе действительного объекта
char* TypeInfo<O.double* o>() {return "double";}
// Информация о типе символьного объекта
char* TypeInfo<O.char* o>() {return "char";}
// Информация о типе строкового объекта
char* TypeInfo<O.str* o>() {return "str";}
//------------------------------------------------------------------------------
// Обобщенный вывод значения объекта с использованием ПП полиморфизма
void Print<O* o>() {printf("Empty object");}
// Вывод целочисленного объекта
void Print<O.int* o>() {printf("%d", o->@);}
// Вывод действительного объекта
void Print<O.double* o>() {printf("%le", o->@);}
// Вывод символьного объекта
void Print<O.char* o>() {printf("%c", o->@);}
// Вывод строкового объекта
void Print<O.str* o>() {printf("%s", o->@);}
Вариант, связанный с порождением разреженных параметрических отношений можно рассмотреть на примере функции сложения целых и действительных чисел, определяемых соответствующим специализациями. Пусть используются только две операции сложения:
сложение целого числа с целым, порождающее на выходе целое;
сложение действительного числа с действительным, порождающее действительное.
// Использование ПП полиморфизма и явной проверки для обобщенных объектов
// Суммирование целого с целым с порождением обобщенного
O* AddIntInt(struct O.int* o1, struct O.int* o2) {
struct O.int* r = create_spec(O.int);
r->@ = o1->@ + o2->@;
return (O*)r;
}
// Суммирование действительного с действительным с порождением обобщенного
O* AddDoubleDouble(struct O.double* o1, struct O.double* o2) {
struct O.double *r = create_spec(O.double);
r->@ = o1->@ + o2->@;
return (O*)r;
}
При реализации мультиметода с использованием 4П необходимо для этих комбинаций реализовать только два обработчика специализаций.
// Суммирование с использованием мультиметода
O* Add<O* o1, O* o2>(){return create_spec(O);}
O* Add<O.int* o1, O.int* o2>(){return AddIntInt(o1, o2);}
O* Add<O.double* o1, O.double* o2>(){return AddDoubleDouble(o1, o2);}
При этом формируемая таблица параметрических отношений должна иметь размер, учитывающий помимо числовых комбинаций еще и все другие специализации. Вряд ли такое решение, особенно при большом количестве специализаций, можно считать эффективным. Правда, иногда размер имеет значение в ущерб скорости.
От полиморфизма к явной проверки типов
Одной из простых альтернатив является явная проверка "стертого типа" во время выполнения с использованием, например, полиморфной операции TypeInfo. В этом случае напрямую выявляются оба варианта функций, осуществляющих суммирование.
// Суммирование с использованием информации о типах
O* Add2(O* o1, O* o2) {
if(!strcmp(TypeInfo<o1>(),"int") && !strcmp(TypeInfo<o2>(), "int")) {
return AddIntInt((struct O.int*)o1, (struct O.int*)o2);
}
if(!strcmp(TypeInfo<o1>(),"double") && !strcmp(TypeInfo<o2>(), "double")) {
return AddDoubleDouble((struct O.double*)o1, (struct O.double*)o2);
}
return create_spec(O);
}
Приведенная выше проверка типа специализации использует сравнение строк символов, что в целом громоздко. Вместо этого можно использовать функцию PPC, осуществляющую сравнение индексов специализаций, известных во время выполнения. Сравнение осуществляется с образцом, который может быть реализован как локальная переменная функции или как статическая переменная. Последнее немного ускоряет обработку за счет формирования признака до начала выполнения программы. При этом проверки на конкретный тип можно делать выборочно.
_Bool isO(O* o) {
return spec_index_cmp(o, o) == 0;
}
// Проверка, что объект целочисленный
_Bool isIntO(O* o) {
struct O.int i; // использование локальной переменной
return spec_index_cmp(o, (O*)&i) > 0;
}
// Проверка, что объект действительный
static struct O.double d; // использование статической переменной
_Bool isDoubleO(O* o) {
return spec_index_cmp(o, (O*)&d) > 0;
}
В этом случае обобщенная функция суммирования будет выглядеть следующим образом.
// Суммирование с использованием прямой проверки типов
O* Add3(O* o1, O* o2) {
if(isIntO(o1) && isIntO(o2)) {
return AddIntInt((struct O.int*)o1, (struct O.int*)o2);
}
if(isDoubleO(o1) && isDoubleO(o2)) {
return AddDoubleDouble((struct O.double*)o1, (struct O.double*)o2);
}
return create_spec(O);
}
Приведенные варианты позволяют избавиться от разреженных параметрических отношений. Однако они ведут к созданию функций, собирающих альтернативы воедино, и обеспечивающих их явную проверку. Даже если такие функции будут формироваться в ходе трансформации процедурно-параметрического мультиметода, их необходимо будет порождать только после того, как известны все обработчики специализаций, которые могут находиться в разных единицах компиляции. То есть генерация подобных функций (причем необязательно на исходном языке) должна предшествовать этапу компоновки или осуществляться непосредственно в ходе нее.
Алгоритмы вместо явной проверки типов
Применение алгоритмов, использующих для определения специализаций быстрый поиск или хеширование, может при определенных условиях оказаться предпочтительнее явной проверки типов. Скорее это будет наблюдаться при числе обработчиков специализаций в среднем диапазоне. В качестве примера подобного алгоритма для суммирования можно привести простой словарь на пару элементов, который технически может быть построен как во время компоновки, так и во время начальной инициализации программы.
//------------------------------------------------------------------------------
// Построение простого демонстрационного словаря
typedef struct Pair {int key; O*(*f)(O* o1, O* o2);} Pair;
typedef struct Map {Pair p[2];} Map;
// Алгоритм заполения словаря для сложения технически можно разнести
void CreateMap(Map* m) {
// Добавление в словарь информации о целочисленном сложении
struct O.int i;
int key = spec_index_cmp(&i, &i);
m->p[0].key = key + (key << 8); // будет достаточно
m->p[0].f = AddIntIntO;
// Добавление в словарь информации о сложении действительных чисел
struct O.double d;
key = spec_index_cmp(&d, &d);
m->p[1].key = key + (key << 8); // будет достаточно
m->p[1].f = AddDoubleDoubleO;
}
// Суммирование с использованием словаря для выбора обработчика
O* Add4(Map* m, O* o1, O* o2) {
for(int i = 0; i <2; ++i) {
if(m->p[i].key ==
(spec_index_cmp(o1, o1)) + (spec_index_cmp(o2, o2) << 8)) {
return m->p[i].f(o1, o2);
}
}
return create_spec(O);
}
В представленном коде ключ для словаря формируется путем сдигов признаков для первого и второго операндов. При необходимости отдельные компоненты формирования разных ключей и привязка к ним функций сложения могут быть разнесены по различным единицам компиляции. Формируемый словарь используется для поиска нужного обработчика специализации непосредственно при выполнении функции суммирования. Хеширование можно реализуется аналогичным способом.
В тестовой программе приведены различные варианты суммирования. Представлены также ситуации с некорректными типами операндов.
int main() {
Map m;
CreateMap(&m);
O*o ;
O* X[10];
struct O.int i0; i0.@ = 10; X[0] = (O*)&i0; Print<(O*)X[0]>(); printf("\n");
struct O.int i1; i1.@ = 32; X[1] = (O*)&i1; Print<(O*)X[1]>(); printf("\n");
o = Add<X[0], X[1]>(); Print<o>(); printf("\n"); free(o);
o = Add2(X[0], X[1]); Print<o>(); printf("\n"); free(o);
o = Add3(X[0], X[1]); Print<o>(); printf("\n"); free(o);
o = Add4(&m, X[0], X[1]); Print<o>(); printf("\n"); free(o);
struct O.double d0; d0.@ = 2.71; X[2] = (O*)&d0; Print<(O*)X[2]>(); printf("\n");
struct O.double d1; d1.@ = 3.14; X[3] = (O*)&d1; Print<(O*)X[3]>(); printf("\n");
o = Add<X[2], X[3]>(); Print<o>(); printf("\n"); free(o);
o = Add2(X[2], X[3]); Print<o>(); printf("\n"); free(o);
o = Add3(X[2], X[3]); Print<o>(); printf("\n"); free(o);
o = Add4(&m, X[2], X[3]); Print<o>(); printf("\n"); free(o);
o = Add<X[1], X[2]>(); Print<o>(); printf("\n"); free(o);
o = Add2(X[3], X[0]); Print<o>(); printf("\n"); free(o);
o = Add3(X[3], X[1]); Print<o>(); printf("\n"); free(o);
o = Add4(&m, X[0], X[3]); Print<o>(); printf("\n"); free(o);
return 0;
}
Использование алгоритмических решений также требует генерации дополнительного кода в ходе сборки программы. Помимо этого возможно связывание с признаками специализаций уже во время начальной загрузки, если эти признаки формируются только на стадии выполнения. Вместе с тем, при тщательном проектировании генератора кода для них можно добиться более гибкой и эволюционной расширяемости по сравнению с использованием явной проверки типов.
Параметризация вместо виртуализации
Виды динамического полиморфизма отличаются по механизму реализации. Дополнительными факторами их изменчивости могут служить особенност привязки к различным вариантам организации данных и функций. При этом внешняя языковая оболочка может соответствовать уже существующим решениям при измененной внутренней организации механизма поддержки полиморфизма. Ради хохмы можно попробовать, используя C++, смоделировать ОО полиморфизм без использования виртуализации, заменив ее на параметрические таблицы. Вновь обратимся к геометрическим фигурам. В качестве примера приводится один из вариантов, размещенных в репозитории.
Класс обобщенной фигуры, лишенный виртуальных методов, выглядит следующим образом.
//------------------------------------------------------------------------------
// Класс, обобщающает все имеющиеся фигуры.
// Является абстрактным, обеспечивая, тем самым, проверку интерфейса
class Figure {
public:
// Идентификация, порождение и ввод фигуры из потока
static Figure* InAny(std::ifstream &ifst);
// Метод ввода фигуры через параметрическую таблицу
void In(std::ifstream &ifst);
// Метод вывода фигуры через параметрическую таблицу
void Out(std::ofstream &ofst);
// Формирование параметрических отношений вместо виртуальных методов
int specTag; // Признак специализации, доступный из обобщения
// Счетчик числа зарегистрированных специализаций
static int specCounter;
};
Он содержит обычные методы In и Out, которые являются точками входа в пространство статических методов производных классов, спрятанных в параметрических таблицах. Эти статические методы "заменяют" виртуальные методы ввода и вывода, используемые при обычной ОО реализации. Также данный класс содержит признак специализации для обобщенной фигуры (specTag) и статический счетчик числа зарегистрированных специализаций (specCounter). Статический метод (InAny), как и в случае обычного ОО решения, используется для ввода фигур различного типа из файла.
Формирование полиморфных отношений для базового и производных классов осуществляется с использованием параметрических таблиц, реализованных в данном случае через std::vector<InDataP> для ввода специализаций и std::vector<OutDataP> для их вывода.
// Параметрическая таблица для функций ввода
// static std::vector<void (*)(Figure* f, std::ifstream &ifst)> InP;
extern std::vector<InDataP> InP;
// Параметрическая таблица для функций вывода
// static std::vector<void (*)(Figure* f, std::ofstream &ofst)> OutP;
extern std::vector<OutDataP> OutP;
Функции, подключаемые через указатели, имеют следующие сигнатуры.
// Общее описание типа обобщающих функций
typedef void (*InDataP)(Figure* f, std::ifstream &ifst);
typedef void (*OutDataP)(Figure* f, std::ofstream &ofst);
Реализации методов и функций, поддерживающих обобщенную фигуру, выглядят следующим образом.
//------------------------------------------------------------------------------
// Счетчик числа зарегистрированных специализаций
int Figure::specCounter = 0;
//------------------------------------------------------------------------------
// Параметрическая таблица для функций ввода
std::vector<InDataP> InP;
// Параметрическая таблица для функций вывода
std::vector<OutDataP> OutP;
//------------------------------------------------------------------------------
// Метод ввода фигуры через параметрическую таблицу
void Figure::In(std::ifstream &ifst) {
InP[specTag](this, ifst);
}
//------------------------------------------------------------------------------
// Метод вывода фигуры через параметрическую таблицу
void Figure::Out(std::ofstream &ofst) {
OutP[specTag](this, ofst);
}
Отдельно реализуется ввод фигур из файла. Единственной функции, которая изменяется при добавлении новых фигур.
//------------------------------------------------------------------------------
// Ввод параметров обобщенной фигуры из стандартного потока ввода
Figure* Figure::InAny(std::ifstream &ifst) {
Figure *sp;
int k;
ifst >> k;
switch(k) {
case 1:
sp = new Rectangle;
break;
case 2:
sp = new Triangle;
break;
default:
return 0;
}
sp->In(ifst);
return sp;
}
Разработка производных классов осуществляется с использованием наследования, что обеспечивает для них общую регистрацию и возможность подключения дополнительных обработчиков специализаций к общим параметрическим таблицам. В частности, описание класса прямоугольника будет выглядеть следующим образом.
//------------------------------------------------------------------------------
// прямоугольник
class Rectangle: public Figure {
int x, y; // ширина, высота
public:
Rectangle();
// Функция ввода содержимого прямоугольника,
// подключаемая через параметрическую таблицу
static void In(Figure* f, std::ifstream &ifst);
// Функция вывода содержимого прямоугольника,
// подключаемая через параметрическую таблицу
static void Out(Figure* f, std::ofstream &ofst);
// Признак фигуры
static int tag;
};
Внутри его определяются статические функции, подключаемые к своим параметрическим таблицам, определенным для фигуры и статическое поле признака, общее для всех прямоугольников. Реализация этих методов и инициализация статического поля представлены далее.
//------------------------------------------------------------------------------
// Начальное значение признака специализации до регистрации
int Rectangle::tag = -1;
//------------------------------------------------------------------------------
// Конструктор, обеспечивающий формирование прямоугольника,
// установку признака и регистрацию в параметрических таблицах
Rectangle::Rectangle(): x{0}, y{0} {
// std::cout << "Rectangle 01: Tag = " << tag << "\n";
if(tag == -1) {
// Установка тега и размещение в параметрических таблицах
tag = specCounter++;
InP.push_back(Rectangle::In);
OutP.push_back(Rectangle::Out);
}
specTag = tag; // Инициализация собственного тега объекта
// В противном случае регистрация уже состоялась
// Тестовый вывод результатов регистрации
// std::cout << "Rectangle tag = " << specTag << "\n";
}
//------------------------------------------------------------------------------
// Ввод содержимого прямоугольника,
// подключаемый через параметрическую таблицу
void Rectangle::In(Figure* f, std::ifstream &ifst) {
Rectangle* r = reinterpret_cast<Rectangle*>(f);
ifst >> r->x >> r->y;
}
//------------------------------------------------------------------------------
// Вывод содержимого прямоугольника,
// подключаемый через параметрическую таблицу
void Rectangle::Out(Figure* f, std::ofstream &ofst) {
Rectangle* r = reinterpret_cast<Rectangle*>(f);
ofst << "It is Rectangle: x = " << r->x << ", y = " << r->y << "\n";
}
Аналогичным образом формируются описания и реализации остальных производных фигур, добавляемых в программу.
Контейнер, а также главная функция не используют конкретных фигур и остаются такими же, как и в программах демонстирирующих прямое расширение кода при виртуализации и наследовании.
//------------------------------------------------------------------------------
// Простейший контейнер на основе одномерного массива
//------------------------------------------------------------------------------
class Container {
enum {max_len = 100}; // максимальная длина
int len; // текущая длина
Figure *cont[max_len];
public:
void In(std::ifstream &ifst); // ввод фигур в котнейнер из входного потока
void Out(std::ofstream &ofst); // вывод фигур в выходного потока
void Clear(); // очистка контейнера от фигур
Container(); // инициализация контейнера
~Container() {Clear();} // утилизация контейнера перед уничтожением
};
//------------------------------------------------------------------------------
// Инициализация контейнера
Container::Container(): len{0} { }
//------------------------------------------------------------------------------
// Очистка контейнера от элементов (освобождение памяти)
void Container::Clear() {
for(int i = 0; i < len; i++) {
delete cont[i];
}
len = 0;
}
//------------------------------------------------------------------------------
// Ввод содержимого контейнера
void Container::In(std::ifstream &ifst) {
while(!ifst.eof()) {
if((cont[len] = Figure::InAny(ifst)) != 0) {
len++;
}
}
}
//------------------------------------------------------------------------------
// Вывод содержимого контейнера
void Container::Out(std::ofstream &ofst) {
ofst << "Container contents " << len << " elements.\n";
for(int i = 0; i < len; i++) {
ofst << i << ": ";
cont[i]->Out(ofst);
}
}
//------------------------------------------------------------------------------
// Главная функция программы
int main(int argc, char* argv[]) {
if(argc !=3) {
std::cout << "incorrect command line! Wated: command infile outfile\n";
return 1;
}
std::ifstream ifst(argv[1]);
std::ofstream ofst(argv[2]);
std::cout << "Start\n";
Container c;
c.In(ifst);
ofst << "Filled container.\n";
c.Out(ofst);
c.Clear();
ofst << "Empty container.\n";
c.Out(ofst);
std::cout << "Stop\n";
return 0;
}
Рассмотренная упрощенная модель позволяет вызывать методы объектов, порождаемых от базового и производного классов аналогично тому, как это делается при использовании виртуализации, демонстрируя тем самым, что возможны и другие пути поддержки ОО полиморфизма.
Заключение
Разнообразие подходов к представлению динамического полиморфизма на уровне языков программирования и механизмов их внутренней реализации позвляет использовать различные мультипарадигменные комбинации как при разработке программ, так и при создании систем программирования. При этом эквиалентные трансформации различных способов обработки альтернатив во время выполнения могут быть оптимизированы в ходе предварительного анализа, учитывающие различные параметры программы. То есть, такому анализу и преобразованию может быть подвержен не только статический, но и динамический полиморфизм, что дает дополнительные возможности при разработке компиляторов.
Помимо этого интересно рассмотреть такие моменты, как совместное использование различных видов полиморфизма для решения тех или иных задач. Уже существуют языки, которые сочетают ОО полиморфизм с множественным полиморфизмом, реализованным на основе хеширования или быстрого поиска. Было бы любопытно взглянуть на возможность использования над классами для реализации множественного полиморфизма процедурно-параметрических решений. В целом вариантов развития методов и техники программирования просматривается еще много, если двигаться в сторону мультипарадигменного стиля.
Список используемых источников
[PPC] Легалов А.И., Косов П.В. Процедурно-параметрическое расширение языка программирования C. Синтаксис и семантика
[AKay2003en] Dr. Alan Kay on the Meaning of “Object-Oriented Programming” - оригинал
[AKay2003ru] Доктор Алан Кей о смысле «объектно-ориентированного программирования» - перевод
[AKey1997] Alan Kay at OOPSLA 1997 - The computer revolution hasnt happened yet
[OOP] Википедия. Объектно-ориентированное_программирование
[Badd] Бадд Т. Объектно-ориентированное программирование в действии. /Пер. с англ. - СПб: Питер, 1997 - 464 с.
[LKKP] Легалов А.И., Казаков Ф.А., Кузьмин Д.А., Привалихин Д.В. Функциональная модель параллельных вычислений и язык программирования "Пифагор" - 2002-2003.
[PP-patterns] Легалов А.И. Процедурно-параметрическая парадигма и паттерны ОО проектирования - 2025.
[OOP-PPP] Косов П.В., Легалов А.И. Сравнение объектно-ориентированного и процедурно-параметрического полиморфизма. Труды Института системного программирования РАН. 2025;37(6):43-58.
https://doi.org/10.15514/ISPRAS-2025-37(6)-3.
https://ispranproceedings.elpub.ru/jour/article/view/2043/1865
[MM] Легалов А.И., Косов П.В. Расширение языка C для поддержки процедурно-параметрического полиморфизма. Моделирование и анализ информационных систем. 2023;30(1):40-62. https://doi.org/10.18255/1818-1015-2023-1-40-62.
[ZIG] Karl Seguin. Learning Zig.
[Голуб] Голуб А.И. C, C++. Правила программирования. М: Бином. 1996. - 272 с.
[DHP] Легалов А.И. Разнорукое программирование. Как "прикинуться" объектным обобщением - 2001.
[PPMODEL] Легалов А.И. Эволюция мультиметодов при процедурном подходе - 2002.
|