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 через
инициализацию переменных, что в свою очередь заставляет все переменные
выносить вверх функции и инициализировать хоть чем-нибудь, чтоб избавиться
от предупреждений компилятора, и такой подход в целом, на мой взгляд
ещё только увеличивает возможность возникновения ошибок по целой массе разных
причин...
Здравствуйте, fk0, Вы писали:
fk0> Существует возможность привести такой код к следующей форме:
fk0>fk0> SCOPE_GUARD
fk0> {
fk0> void * const p = malloc(25);
fk0> DEFER(free, p);
fk0> void * const q = malloc(25);
fk0> DEFER(free, q);
fk0> if (mtx_lock(&mut) == thrd_error)
fk0> return or break;
fk0> DEFER(mtx_unlock, &mut);
fk0> }
fk0>
Что мешает использовать менеджер ресурсов, который будет следить за выделением ресурсов и он же будет их освобождать.
Выглядеть будет примерно так
static void body_code(scope_t *s) {
void *p, *q; mutex_t m[1];
p=scope_mem_alloc(s,25);
if (scope_mutex_lock(s,m)) return;
q=scope_mem_alloc(s,25);
}
void body(scope_t *s) {
scope_exec(s,body_code);
}
Здравствуйте, kov_serg, Вы писали:
_>Что мешает использовать менеджер ресурсов, который будет следить за выделением ресурсов и он же будет их освобождать.
_>Выглядеть будет примерно так
_>_>static void body_code(scope_t *s) {
_> void *p, *q; mutex_t m[1];
_> p=scope_mem_alloc(s,25);
_> if (scope_mutex_lock(s,m)) return;
_> q=scope_mem_alloc(s,25);
_>}
_>void body(scope_t *s) {
_> scope_exec(s,body_code);
_>}
_>
Это хорошо работало бы в языках, где есть лямбды. Или даже в обычном (Turbo) паскале,
где есть понятие вложенных функций. А в голом C неудобно, так как логика разбивается
на большое количество функций, где за логикой собственно уже и не уследить. Ведь
для каждого вложенного scope нужна отдельная функция.
Вдогонку, важное забыл. Для лямбды так или иначе важен КОНТЕКСТ. Который она
автоматически захватывает. А в данном случае функция body_code вынуждена
контекст принимать в аргументах. Так программировать в целом крайне неудобно...
В C есть лямбды, но это или бестолковое GCC-расширение добавляющее вложенные
функции (с трамплинами, исполняемым стеком...), либо "Blocks" из clang. Т.е. все
подобного рода расширения, к сожалению, специфические только для одного конкретного
компилятора.