Аналог вложенным функциям на языке программирования Си

от автора

Полноценной замены вложенным функциям в языке программирования Си нет, но есть несколько способов, как их можно симулировать. Чаще всего в вложенных функциях нам важно то, что код определяется там же, где передаётся в качестве функции обратного вызова. Иногда этот код бывает настолько мал, что выносить его в отдельную функцию в глобальной области видимости смысла нет. Например, для сортировки массива по возрастанию с помощью функции типа qsort чаще всего достаточно такого кода: return e1 - e2;. Вынести его в отдельную функцию в глобальной области видимости, а затем ещё придумывать корректное название — так себе удовольствие. Вложенные функции, добавленные в GCC как расширение, могли бы решить эту проблему, но такой код не будет работать на других компиляторах языка Си.

Способ, который описан в этой статье, основан на возврате из функции вместо обратного вызова и работает примерно так же, как функции-генераторы или цикл событий. Для начала взглянем на код и сравним его с вложенными функциями из GCC.

int array[] = {1, 4, 2, 5, 2, 2, 4, 6}; int n = 8;  //GCC расширение int removePred(int e){ return e % 2; } n = remove_if(array, n, removePred);   {func(remove_if, array, n){ ths.ret = ths.e % 2; } n = ths.res; } 

Здесь мы использовали макрос func, который сокращает нашу запись, а вот во что он превратиться после обработки препроцессором.

{   struct remove_if ths = {0, array, n};   while(remove_if(&ths)){     ths.ret = ths.e % 2 == 1;   }   n = ths.res; }

Логика для выявления нечётных чисел расположена внутри вызывающей функции, а чтобы получить к ней доступ, наша функция remove_if возвращает управление, запоминая состояние внутри структуры ths, а возвращаемым значением сообщает, что её работа не окончена. Фигурные скобки используются для ограничения области видимости, чтобы предотвратить конфликт имен, но можно избавиться от них — тогда наш код примет вид.

func(remove_if, obj, array, n){ obj.ret = obj.e % 2 == 1; } n = obj.res;

Весь код будет выглядеть так

#include <stdio.h>  struct remove_if{   int st;   int *arr;   int n;   int e;   int res;   int ret;   int i;   int ofs; };  /* Суть алгоритма ниже в этом int remove_if(Callback callback, int* array, int n){   int ofs = 0, i = 0;   for(; i < n; i++){     if(callback(array[i]){       ofs++;     } else {       arr[i - ofs] = arr[i];     }   }   return i - ofs; } */  int remove_if(struct remove_if *r){   if(r->st == 0){     r->st = 1;     r->i = 0;     r->ofs = 0;   }else{     if(r->ret){       r->ofs++;     }else{       r->arr[r->i - r->ofs] = r->arr[r->i];     }     r->i++;   }   if(r->i == r->n){     r->res = r->i - r->ofs;     return 0;   }   r->e = r->arr[r->i];   return 1; }  #define func(fn, ...)\   struct fn ths = {0, __VA_ARGS__};\   while(fn(&ths))  void printArray(int *array, int n){   for(int i = 0; i < n; i++){     printf("%d, ", array[i]);   }   printf("\n"); }  int main(){    int array[10] = [5, 2, 8, 6, 5, 2, 1, 1, 2, 3];   int n = 10;    {func(remove_if, array, n){     ths.ret = ths.e % 2 == 1;   } n = ths.res; }    printArray(array, n);    return 0; }

Функция для удаления элементов выглядит немного необычно, но производительность будет примерно такой же, как при использовании вложенных функций, так как операций происходит примерно одинаковое количество, вызовов и возвращений из функций происходит тоже равное количество. Возможно наш код лишится некоторых оптимизаций, но не критических, в случае необходимости мы можем даже использовать inline. Но у этого способа есть другой недостаток  —  у нас нет указателя на функцию, следовательно мы не можем запомнить её, чтобы вызвать позже. Также способ немного необычный, но в этой необычности есть своя логика и чтобы разобраться в ней, взглянем на следующий код.

typedef void (*func)();  int template(func f, int a1, int a2){   int b, c, d;   //код до цикла   while(/*условие выхода из цикла*/0){     //код до вызова обратной функции внутри цикла     int ret = f();     //код после вызова обратной функции внутри цикла   }   //код после цикла   return 5; //возвращаемое значение }  struct template{   int st; //состояние первый вызов = 0, повторный вызов = 1   int a1, a2; //аргументы   int a, b, c; //переменные   int ret; //возвращаемое обратной функцией значение   int res; //возвращаемое значение };  int template(struct template *t){   if(t->st == 0){     //инициализация     //код до цикла   }else{     //код после вызова обратной функции внутри цикла   }   if(/*условие выхода из цикла*/0){     //код после цикла     t->res = 5;//возвращаемое значение     return 0;   }   //код до вызова обратной функции внутри цикла   return 1; }

Здесь сравнивается простой шаблон, по которому мы можем создать функцию. Комментарии показывают, какая часть соответствует которой. Все переменные мы храним в структуре, так как они нам нужный при повторном вызове. state указывает, это первый вызов или повторный, если простыми словами. Это простой шаблон с одним циклом и вызовом внутри цикла. Его можно расширить под нужды. В следующем коде собраны несколько алгоритмов: remove_if, map и сортировка пузырьком bsort.

#include <stdio.h> #include <stdlib.h>  struct remove_if{   int st;   int *arr;   int n;   int e;   int res;   int ret;   int i;   int ofs; };  int remove_if(struct remove_if *r){   if(r->st == 0){     r->st = 1;     r->i = 0;     r->ofs = 0;   }else{     if(r->ret){       r->ofs++;     }else{       r->arr[r->i - r->ofs] = r->arr[r->i];     }     r->i++;   }   if(r->i == r->n){     r->res = r->i - r->ofs;     return 0;   }   r->e = r->arr[r->i];   return 1; }  struct map{   int st;   int *arr;   int n;   int ret;   int *res;   int i, e; };  int map(struct map* m){   if(m->st == 0){     m->i = 0;     m->st = 1;     m->res = malloc(sizeof(int) * m->n);   }else{     m->res[m->i] = m->ret;     m->i++;   }   if(m->i == m->n){     return 0;   }   m->e = m->arr[m->i];   return 1; }  struct bsort{   int st;   int *arr;   int n;   int i, j;   int e1, e2;   int ret; };  int bsort(struct bsort *s){   if(s->st == 0){     s->st = 1;     s->i = 1;     s->j = 0;   }else{     if(s->ret > 0){       int tmp = s->arr[s->j];       s->arr[s->j] = s->arr[s->j + 1];       s->arr[s->j + 1] = tmp;     }     s->j++;     if(s->j >= s->n - s->i){       s->j = 0;       s->i++;     }   }   if(s->i >= s->n){     return 0;   }   s->e1 = s->arr[s->j];   s->e2 = s->arr[s->j + 1];   return 1; }  #define func(fn, ...)\   struct fn ths = {0, __VA_ARGS__};\   while(fn(&ths))  void printArray(int *array, int n){   for(int i = 0; i < n; i++){     printf("%d, ", array[i]);   }   printf("\n"); }  int main(){    int array[10] = {5, 2, 8, 6, 5, 2, 1, 1, 2, 3};   int n = 10;    printArray(array, n);    {func(remove_if, array, n){     ths.ret = ths.e % 2 == 1;   } n = ths.res; }    printArray(array, n);    {func(bsort, array, n){     ths.ret = ths.e1 - ths.e2;   }}    printArray(array, n);    int *arr2;    {func(map, array, n){     ths.ret = ths.e * 2;   } arr2 = ths.res; }    printArray(arr2, n);    free(arr2);    return 0; } 

Многие вещи в языке программирования Си создаются на уровне кода: сопрограммы, исключения и даже ООП. Такие конструкции почти всегда уступают встроенным средствам как по гибкости, так и по производительности. Иногда они используются в крупных проектах, как например GObject, а иногда остаются творческими проектами, которые как минимум интересны с образовательной точки зрения, так как именно здесь раскрывается вся гибкость языка и его тонкости.


ссылка на оригинал статьи https://habr.com/ru/articles/944600/