Всё началось с безобидного пролистывания GCC расширений для C. Мой глаз зацепился за вложенные функции. Оказывается, в C можно определять функции внутри функций:
int main() { void foo(int a) { printf("%d\n", a); } for(int i = 0; i < 10; i ++) foo(i); return 0; }
Более того, во вложенных функциях можно менять переменные из внешней функции и переходить по меткам из неё, но для этого необходимо, чтобы переменные были объявлены до вложенной функции, а метки явно указаны через __label__
int main() { __label__ end; int i = 1; void ret() { goto end; } void inc() { i ++; } while(1) { if(i > 10) ret(); printf("%d\n", i); inc(); } end: printf("Done\n"); return 0; }
Документация говорит, что обе внутренние функции валидны, пока валидны все переменные и мы не вышли из области внешней функции, то есть эти внутренни функции можно передавать как callback-и.
Приступим к написанию try-catch. Определим вспомогательные типы данных:
// Данными, как и выкинутой ошибкой может быть что угодно typedef void *data_t; typedef void *err_t; // Определяем функцию для выкидывания ошибок typedef void (*throw_t)(err_t); // try и catch. Они тоже будут функциями typedef data_t (*try_t)(data_t, throw_t); typedef data_t (*catch_t)(data_t, err_t);
Подготовка завершена, напишем основную функцию. К сожалению на хабре нельзя выбрать отдельно язык C, поэтому будем писать try_, catch_, throw_ чтобы их подсвечивало как функции, а не как ключевые слова C++
data_t try_catch(try_t try_, catch_t catch_, data_t data) { __label__ fail; err_t err; // Объявляем функцию выбрасывания ошибки void throw_(err_t e) { err = e; goto fail; } // Передаём в try данные и callback для ошибки return try_(data, throw_); fail: // Если есть catch, передаём данные, над которыми // работал try и ошибку, которую он выбросил if(catch_ != NULL) return catch_(data, err); // Если нет catch, возвращаем пустой указатель return NULL; }
Напишем тестовую функцию взятия квадратного корня, с ошибкой в случае отрицательного числа
data_t try_sqrt(data_t ptr, throw_t throw_) { float *arg = (float *)ptr; if(*arg < 0) throw_("Error, negative number\n"); // Выделяем кусок памяти для результата float *res = malloc(sizeof(float)); *res = sqrt(*arg); return res; } data_t catch_sqrt(data_t ptr, err_t err) { // Если возникла ошибка, печатает её и ничего не возвращаем fputs(err, stderr); return NULL; }
Добавляем функцию main, посчитаем в ней корень от 1 и от -1
int main() { printf("------- sqrt(1) --------\n"); float a = 1; float *ptr = (float *) try_catch(try_sqrt, catch_sqrt, &a); if(ptr != NULL) { printf("Result of sqrt is: %f\n", *ptr); // Не забываем освободить выделенную память free(ptr); } else printf("An error occured\n"); printf("------- sqrt(-1) -------\n"); a = -1; ptr = (float *)try_catch(try_sqrt, catch_sqrt, &a); if(ptr != NULL) { printf("Result of sqrt is: %f\n", *ptr); // Аналогично free(ptr); } else printf("An error occured\n"); return 0; }
И, как и ожидалось, получаем
------- sqrt(1) -------- Result of sqrt is: 1.000000 ------- sqrt(-1) ------- Error, negative number An error occured
Try-catch готов, господа.
На этом статью можно было бы и закончить, но тут внимательный читатель заметит, что функция throw остаётся валидной в блоке catch. Можно вызвать её и там, и тогда мы уйдём в рекурсию. Заметим также, что функция throw, это не обычная функция, она noreturn и разворачивает стек, поэтому, даже если вызвать её в catch пару сотен раз, на стеке будет только последний вызов. Мы получаем хвостовую оптимизацию рекурсии.
Попробуем посчитать факториал на нашем try-catch. Для этого передадим указатель на функцию throw в функцию catch. Сделаем это через структуру, в которой также будет лежать аккумулятор вычислений.
struct args { uint64_t acc; throw_t throw_; };
В функции try инициализируем поле throw у структуры, и заводим переменную num для текущего шага рекурсии.
data_t try_(data_t ptr, throw_t throw_) { struct args *args = ptr; // Записываем функцию в структуру, чтобы catch мог её pf,hfnm args->throw_ = throw_; // Заводим переменную для хранения текущего шага рекурсии uint64_t *num = malloc(sizeof(uint64_t)); // Изначально в acc лежит начальное число, в нашем случае 10 *num = args->acc; // Уменьшаем число (*num) --; // Уходим в рекурсию throw_(num); }
В функции catch будем принимать структуру и указатель на num, а дальше действуем как в обычном рекурсивном факториале.
data_t catch_(data_t ptr, err_t err) { struct args *args = ptr; // В err на самом деле лежит num uint64_t *num = err; // Печатаем num, будем отслеживать рекурсию printf("current_num: %"PRIu64"\n", *num); if(*num > 0) { args->acc *= *num; (*num) --; // Рекурсивный вызов args->throw_(num); } // Конец рекурсии // Не забываем осовободить выделенную память free(num); // Выводим результат printf("acc is: %"PRIu64"\n", args->acc); return &args->acc; }
int main() { struct args args = { .acc = 10 }; try_catch(try_, catch_, &args); return 0; }
Вызываем, и получаем, как и ожидалось:
current_num: 9 current_num: 8 current_num: 7 current_num: 6 current_num: 5 current_num: 4 current_num: 3 current_num: 2 current_num: 1 current_num: 0 acc is: 3628800
main.c
#include <stdio.h> #include <stdlib.h> #include <stdint.h> #include <inttypes.h> #include <stdnoreturn.h> typedef void *err_t; typedef void *data_t; typedef void (*throw_t)(err_t); typedef data_t (*try_t)(data_t, throw_t); typedef data_t (*catch_t)(data_t, err_t); data_t try_catch(try_t try, catch_t catch, data_t data) { __label__ fail; err_t err; void throw(err_t e) { err = e; goto fail; } return try(data, throw); fail: if(catch != NULL) return catch(data, err); return NULL; } struct args { uint64_t acc; throw_t throw_; }; data_t try_(data_t ptr, throw_t throw_) { struct args *args = ptr; args->throw_ = throw_; uint64_t *num = malloc(sizeof(uint64_t)); *num = args->acc; (*num) --; throw_(num); } data_t catch_(data_t args_ptr, err_t num_ptr) { struct args *args = args_ptr; uint64_t *num = num_ptr; printf("current_num: %"PRIu64"\n", *num); if(*num > 0) { args->acc *= *num; (*num) --; args->throw_(num); } free(num); printf("acc is: %"PRIu64"\n", args->acc); return &args->acc; } int main() { struct args args = { .acc = 10 }; try_catch(try_, catch_, &args); return 0; }
Спасибо за внимание.
P.S. Текст попытался вычитать, но, так как русского в школе не было, могут быть ошибки. Прошу сильно не пинать и по возможности присылать всё в ЛС, постараюсь реагировать оперативно.
ссылка на оригинал статьи https://habr.com/ru/post/701338/
Добавить комментарий