Пишем try-catch в C не привлекая внимания санитаров

от автора

Всё началось с безобидного пролистывания 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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *