Closures in plain C.
От: fk0 Россия https://fk0.name
Дата: 14.12.21 12:13
Оценка:
Hello...

Известны ли методы реализации замыканий в стандартном (C17) C?

Как я понимаю, поскольку лямбда-функций в язык так и не добавили, то
есть только достаточно ограниченный вариант вроде того, чтобы захватить
указатель на функцию и её аргументы, и получить какое-то подобие
функционального объекта, который теперь можно вызывать.

Но чтоб вызвать функцию с заданными аргументами теперь нужно знать
точный прототип (сигнатуру) вызываемой функции, то есть точно знать и
тип возвращаемого значения и типы аргуменов. Значит функция реализующая
условный operator() функционального объекта должна быть реализована
для всех возможных типов возвращаемых значений и всех возможных типов
аргументов. Что, очевидно, ввиду отсутствия в C шаблонов заставляет
ограничиваться каким-то множеством предварительно сгенерированных
функций для какого-то ограниченного множества типов. Допустим, можно
ограничиться только скалярными встроенными типами, включающими в себя
обощённый указатель (void *), так как для типов заранее неизвестных
функции сгенерировать невозможно (нет шаблонов).

Просматривается общая стратегия реализации замыкания примерно следующая:

1) сам функциональный объект хранит указатель на функцию (в данном
случае тип стирается) и массив unions для аргументов (типы
аргументов тоже не сохранятся);

2) предварительная генерация прототипов и реализаций условной
функции operator() для всех возможных комбинаций возвращаемых
значений и типов аргументов. Ввиду большого объёма, вряд ли
имеет смысл более чем для двух-трёх аргументов.

3) создание многомерного массива, хранящего указатели на сгенерированные
функции, где индексом для каждого измерения является условный индекс
типа, а сами измерения отображают возвращаемое значение и множество
аргументов функции;

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

Вычислить индексы типов можно с помощью операции _Generic (из C11),
отображающей тип заданного выражения на какое-либо непосредственное значение
произвольного (в данном случае enum) типа. В частности тип возвращаемого
значения заданной в аргументах произвольной функции может быть получен
таким же образом.

Генерация функций и прототипов функций может быть осуществлена с помощью
макропроцессора, с помощью известного макроса MAP, отображающего другой
заданный функциональный макрос на множество заданных аргументов
(https://github.com/swansontec/map-macro).

Можно ли в данной схеме что-то упростить, улучшить? Оставясь при этом
в рамках стандартного C, без GNU расширений. Хотя кажется, все они достаточно
бесполезны, кроме может быть __typeof__, который в части случаев заменяется
на _Generic.

Интерес будем считать сугубо академическим. Показать, что в C можно то же
самое, что в языках более высокого уровня.


Зачем это нужно:

Комбинируя в дальнейшем такие функциональные объекты можно привнести
концепцию монад и ленивых вычислений. В дальнейшем это может пригодиться
для реализации стратегии обработки ошибок принятой в других языках,
таких как Visual Basic, Go, C++ и подразумевающей декларативное управление
порядком исполнения (flow control):

1) в Visual Basic возможен переход на заданную метку в случае ошибки
(оператор ON ERROR GOTO);

2) в Go есть оператор defer позволяющий отсрочить выполнение функции;

3) для C++ А. Александреску предложил аналогичную концепцию отложенных
вычислений для обработки ошибок (к счастью C++ достаточно гибкий
язык, чтоб реализовать указанные концепции только в виде библиотеки,
без изменения компилятора):

* CppCon 2015: "Declarative Control Flow"

* C++ and Beyond 2012: Systematic Error Handling in C++


Так же известны реализации механизма отложенных вычислений для языка C
(https://gustedt.wordpress.com/2020/12/14/a-defer-mechanism-for-c/), но
они сейчас не реализуются без изменений компилятора.

Во всех случаях задача отложенных вычислений привести запутанный и сложный,
подверженный ошибкам, порядок вычислений изобилующий goto в явном виде, или
изобилующий дублирующимся кодом, к простому и линейному виду, когда функция
выполняется строго "сверху-вниз" и при этом в декларативном стиле можно
объявить какие операции должны быть выполнены в случае возникновения ошибки
и в случае раннего выхода из функции.

То есть на практическом примере:

{
   void * const p = malloc(25);
   if (!p) goto DEFER0;
   if (false) {
     DEFER1:
       free(p);
       goto DEFER0;
   }

   void * const q = malloc(25);
   if (!q) goto DEFER1;

   if (false) {
     DEFER2:
       free(q);
       goto DEFER1;
   }

   if (mtx_lock(&mut) == thrd_error) goto DEFER2;

   if (false) {
     DEFER3:
       mtx_unlock(&mut);
       goto DEFER2;
   }

   // all resources acquired

   goto DEFER3;
   DEFER0:;
}


Существует возможность привести такой код к следующей форме:

    SCOPE_GUARD
    {
        void * const p = malloc(25);
        DEFER(free, p);

        void * const q = malloc(25);
        DEFER(free, q);

        if (mtx_lock(&mut) == thrd_error)
            return or break;

        DEFER(mtx_unlock, &mut);
    }


Я в курсе, что код представленный на первом примере может быть приведён
к условно-линейной форме с массой меток в конце функции, но такое решение
как бы не ещё хуже, т.к. заставляет "перепрыгивать" с помощью goto через
инициализацию переменных, что в свою очередь заставляет все переменные
выносить вверх функции и инициализировать хоть чем-нибудь, чтоб избавиться
от предупреждений компилятора, и такой подход в целом, на мой взгляд
ещё только увеличивает возможность возникновения ошибок по целой массе разных
причин...
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.