Полноценной замены вложенным функциям в языке программирования Си нет, но есть несколько способов, как их можно симулировать. Чаще всего в вложенных функциях нам важно то, что код определяется там же, где передаётся в качестве функции обратного вызова. Иногда этот код бывает настолько мал, что выносить его в отдельную функцию в глобальной области видимости смысла нет. Например, для сортировки массива по возрастанию с помощью функции типа 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, а иногда остаются творческими проектами, которые как минимум интересны с образовательной точки зрения, так как именно здесь раскрывается вся гибкость языка и его тонкости.
Комментарии (12)
apevzner
06.09.2025 15:28Я б так делать не стал. Это очень не по-сишному, поэтому ваш код контринтуитивен и чтобы его понять, придётся внимательно вчитываться в ваши макросы. На мой взгляд, это перевешивает все достоинства.
Khalit Автор
06.09.2025 15:28Код писался как необычный способ как это можно было бы реализовать. В реальном коде лучше придерживаться простоты и читаемости, и избегать сложных конструкций.
Jijiki
06.09.2025 15:28я так делал на С++ в своём пазле - он не продакшен только для меня, ну тут смотря чего хотим добиться, если сплетения в коде переплелись и много проверок то проще забиндить и сверху или в другом файле смотреть(возможно не удобно, но проверки разные бывают) ), так а еще интересное заметил, не знаю как в винде под С, но строка может быть утф32(ну или интереснее утф8+емодзи), всё таки аски скучно как бы не звучало это, жаль что этого пока нету по умолчанию в стандарте
SquareRootOfZero
06.09.2025 15:28В C++, вроде, давно лямбды ввели, а их можно внутри функций локальные объявлять.
Albert2009Zi
06.09.2025 15:28Я проще поинтересуюсь - а зачем это вот всё? Я просто эмбеддер и для программирования мк обхожусь без таких вот приёмов. Где вот без этого, ну вот никак? Не сочтите за негатив, мне реально интересно для расширения кругозора.
ImagineTables
06.09.2025 15:28Я бы сделал так (давно на Си не писал, не серчайте за ошибки):
// Makes a name unique for a context. // fn1 is an 'internal' function, fn2 is the context. #define INTERNAL_FOR(fn1, fn2) fn1##_internal_for_##fn2 bool INTERNAL_FOR(asc, foo1)(int e1, int e2) { return e1 < e2; } void foo1() { … my_sort(my_array1, INTERNAL_FOR(asc, foo1)); … } bool INTERNAL_FOR(asc, foo2)(char* e1, char* e2) { return human_order_compare(e1, e2, OMIT_LEADING_FLAG); } void foo2() { … my_sort(my_array2, INTERNAL_FOR(asc, foo2)); … }
Khalit Автор
06.09.2025 15:28Возможно я не ясно выразился, но когда я сказал сложно придумать имя функции, я имел ввиду сложно придумать имя которое описывало бы действие функции. Разумеется к предикату сравнивающего два элемента это не относится, но если там некоторая формула или более сложное условие, будет проще не только не называть ее, но потом при чтении кода не придется искать определение этой функции или гадать какое действие она выполняет.
ImagineTables
06.09.2025 15:28Так это уже будут анонимные функции (лямбды). А в заголовке — «аналог вложенным». В чём суть вложенности? Не замусоривать глобальный неймспейс и не иметь конфликтов. Что и достигнуто.
Для читабельных лямбд, мне кажется, сишного препроцессинга не хватит.
Khalit Автор
06.09.2025 15:28Не хватит и для полноценных вложенных функций тоже. Но анонимность я считаю тоже достигнута.
RodionGork
понимаю эти чувства но всё же это поиск проблемы там где её нет :) ну придумайте общее правило для конструирования подобных названий и делов-то (имя вызывающей функции и суфикс "sort" и т.п.), функцию объявите как статик и никому не помешает.
ваш подход снижает читабельность (ну она и просто за счет удлиннения внешней функции снижается) - и это перевешивает небольшие потенциальные плюсы, хотя как трюк - занятно
Khalit Автор
Спасибо за комментарий. Я согласен с тем что, это лишь занятный трюк и не ожидаю что кто ни будь, будет использовать его проекте, но проблема такая есть, может быть я её и переоценил.