LINUX.ORG.RU

Си и замыкания

 , ,


1

2

Я тут как-то услышал, что Си поддерживает локальные функции. Сначала не верил, потому, что в плюсах это не работает, однако проверил и был удивлен. Но потом попытался замыкание, и оно тоже заработало. Это конечно хорошо, но почему это работает? Разве локальные функции(в примере «a» и «b») не создаются в стеке и не должны разрушаться по выходу из глобальной функции(в примере «foo»)?

#include <stdio.h>

int (*foo(int key))(int){
	int a(int x){
		return x+1;
	}
	int b(int x){
		return x-1;
	}
	if (key == 0)
		return a;
	else
		return b;
}

int main(){
	int (* f1)(int) = foo(0);
	int (* f2)(int) = foo(1);
	printf("%d %d\n",f1(5),f2(5));
	return 0;
}

★★★★★

Я тут как-то услышал, что Си поддерживает локальные функции. Сначала не верил

и правильно делал, это расширение gcc

Разве локальные функции(в примере «a» и «b») не создаются в стеке

нет

wota ★★
()

Если это у тебя замыкания, то я испанский лётчик. Так попробуй, тебя ждёт сюрприз:

#include <stdio.h>

int (*foo(int key))(int)
{
  int double_key = key*key;
 
  int a(int x){
    return x+double_key;
  }

  int b(int x){
    return x-double_key;
  }

  if (key == 0) {
    return a;
  } else {
    return b;
  }
}

int main()
{
  int (* f1)(int) = foo(0);
  int (* f2)(int) = foo(1);
  printf("%d %d\n",f1(5),f2(5));
  return 0;
}

nanoolinux ★★★★
()

Для начала: «замыкание» — это, по сути, указатель на функцию плюс блок «захваченных» переменных. Функция, сама по себе, ВСЕГДА лежит в секции кода, и НИГДЕ больше.

Допустим у тебя некая функция f пытается внутри себя создать замыкание. В замыкание пихается: указатель на некую уже существующую функцию (из других функций она не видна, но компилятор-то знает про неё всё), а также все локальные (для f) переменные, которые в замыкаемой функции используются.

Когда это замыкание будет вызвано, фактически вызывается по указателю та самая безымянная функция, и ей дополнительно передаётся тот самый блок переменных.

Так происходит в нормальных языках. В C, даже с расширениями, такого не может быть. Там замыканий нет, есть только функции. Но они по-прежнему лежат в секции кода.

У тебя же никаких замыканий тоже нет. Попробуй что-нибудь вроде

int (*foo(int key))(int)
  int a(int x) {
    return x + key;
  }
  return a;
}

Вот переменная key действительно будет лежать на стеке. Поэтому когда возвращённая локальная функция полезет в это место, там уже может быть мусор.

А может и не быть. Ибо то, что лежит на стеке, не «разрушается». Оно остаётся висеть в памяти, но — это место считается теперь свободным, и туда МОЖЕТ БЫТЬ записано что угодно. Поэтому если ты напишешь что-то вроде

int main() {
  int (*f1)(int) = foo(1);
  int (*f2)(int) = foo(2);
  printf("%d\n", f1(0));
...
то, с хорошей вероятностью, увидишь «2». Потому что новый key=2 будет положен в то же место, что и прежний key=1, и перезапишет его.

А можешь увидеть вообще хрен знает что, если повезёт.

Miguel ★★★★★
()
Ответ на: комментарий от nanoolinux

Не знаете, почему эту фишку выпилили в С++?

Этой фишки и в C не было.

theNamelessOne ★★★★★
()

Разве локальные функции(в примере «a» и «b») не создаются в стеке

ну дык ты сам сделал замыкание. Вот они и создаются НЕ в стеке(или в стеке, но в другом фрейме, который выше).

ЗЫЖ теперь расскажи, на кой ляд это нужно?

emulek
()
Ответ на: комментарий от Miguel

то, с хорошей вероятностью, увидишь «2»

это если только с -O0. А обычно она инлайтит и по регистрам распихивает.

А можешь увидеть вообще хрен знает что, если повезёт.

в 146% случаев «везёт». IRL с -O0 никто не собирает.

emulek
()
Ответ на: комментарий от Aswed

Не знаете, почему эту фишку выпилили в С++?

лучше скажите, на кой её впилили?

emulek
()
Ответ на: комментарий от hvatitbanit

Как вы представляете замыкания без GC?

А ты подумай. Быдлокодеры совсем отупели.

anonymous
()
Ответ на: комментарий от hvatitbanit

Как вы представляете замыкания без GC?

1. даже в асме x86 есть GC. Стек называется.

2. что тебе мешает сделать GC в C++ СБИШ?

emulek
()
Ответ на: комментарий от hvatitbanit

Трудно, но возможно, кстати говоря. GC — не единственная возможность. Можно сделать, например, region-based memory management. Можно заюзать capability calculus.

Miguel ★★★★★
()
Ответ на: комментарий от Miguel

Можно сделать, например, region-based memory management

Что-то не особо понял идею (и гугл пока не помог, хотя гуглил минуты 2 всего). Как говорит википедия, регион - это некоторая область памяти, которая может несколько раз пополняться, а потом быстро освобождена за раз (как я понял). Там ещё был пример со списком.

А как понять, когда его освобождать в случае с замыканиями?

hvatitbanit
()
Ответ на: комментарий от emulek

даже в асме x86 есть GC. Стек называется.

Стек - это не GC. Забыл из него достать и привет. Другое дело что в высокоуровневых языках обычно в стек ручками не лезут(низкоуровневая анальщина не в счёт) и стеком заведует исключительно компилятор. Но всё равно, сравнение мягко говоря натянутое.

Pinkbyte ★★★★★
()
Ответ на: комментарий от hvatitbanit

Основных идей там три.

1) Любое значение — будь оно обычным числом, записью, замыканием етц — лежит в каком-то регионе. При его создании регион указывается. Само по себе значение не удаляется из памяти, удаляется только целый регион.

2) Регионы не выделяются или удаляются специально; вместо этого они используют лексическую область видимости. Регион объявлен — значит, его можно использовать; закончилась область его видимости — регион удаляется. При этом не обязательно добавлять новые значения в последний объявленный регион, можно в любой.

3) Регионы могут быть аргументами функций. То есть «эта функция принимает два региона r1 и r2, а также значение x, лежащее в r1; она возвращает некоторое значение, лежащее в r2».

При этом регионы НИКОГДА не прячутся в замыканиях, и ВСЕГДА являются частью типа функции. То есть, если у тебя есть некоторое замыкание, использующее некий регион, оно ВООБЩЕ не может использоваться после того, как регион сдохнет. Потому что у него никакого осмысленного типа не будет. Оно может ещё висеть в памяти, но воспользоваться им уже нельзя.

Пример (очень условный и на псевдокоде):

with_region r {
  with_region s {
    int z at s : int;
    function f at r : () -> (y at s : int) {
      return z;
    }
    // В этом месте функцию f можно использовать
  }
  // В этом месте никаким способом достучаться до f нельзя.
  // Невозможна, например, переменная, хранящая указатель на f,
  // так как тип такой переменной будет включать в себя регион s,
  // а он уже не виден.
  //
  // При этом сама f остаётся в регионе r, который ещё существует.
}
Miguel ★★★★★
()
Ответ на: комментарий от hvatitbanit

перечитал. В чём проблема-то? Чем тебе не нравятся замыкания в C++11?

emulek
()
Ответ на: комментарий от hvatitbanit

А как понять, когда его освобождать в случае с замыканиями?

функция живёт в некотором контексте. Вместе со своими переменными. В сишечке изначально два контекста: фрейм стека функции, и глобальный. Вот «замыкание» это костыль, который разрешает доступ в чужой фрейм, вызывающей функции.

вот пример нагуглил: http://rsdn.ru/article/funcprog/fp.xml#E3GAE

Очищается фрейм как обычно, на выходе функции. Но другой функции.

emulek
()
Ответ на: комментарий от Pinkbyte

Стек - это не GC.

да, конечно

void foo()
{
  int a[100];
}// здесь массив a очищается.

Другое дело что в высокоуровневых языках обычно в стек ручками не лезут(низкоуровневая анальщина не в счёт) и стеком заведует исключительно компилятор. Но всё равно, сравнение мягко говоря натянутое.

знаешь, если я в java/php руками полезу в gc, то у меня тоже ничего хорошего не получится.

emulek
()
Ответ на: комментарий от Miguel

При этом регионы НИКОГДА не прячутся в замыканиях, и ВСЕГДА являются частью типа функции

А, в этом и секрет. Весьма неудобно выходит, хоть и не реально

hvatitbanit
()
Ответ на: комментарий от emulek

Как эти данные, что ухватились в замыкание могут жить на стеке? Я вас таки решительно не понимаю.

#include <stdio.h>

void* lol (int a)
{
    int b (int c)
    {
        return a + c;
    }
    return b;
}

int main()
{
    int (*ptr1) (int);
    int (*ptr2) (int);
    ptr1 = lol (4);
    ptr2 = lol (10);
    printf ("%i %i\n", ptr1(4), ptr2(4));
}

14 14

hvatitbanit
()
Ответ на: комментарий от hvatitbanit

На самом деле, поскольку можно делать регион аргументом функции, получается весьма гибкая схема. Т.е., можно делать что-то вроде такого:

function id at r : (region s, x at s : int) -> y at s : int {
  return x;
}
и после этого вызывать её с разными регионами:
with_region s1 {
  x at s1 : int = 1;
  print(id(s1, x));
}
with_region s2 {
  y at s2 : int = 2;
  print(id(s2, y));
}

Miguel ★★★★★
()
Ответ на: комментарий от Pinkbyte

А теперь выдели память через malloc и повтори

а теперь в своём php выдели память через malloc(3), и посмотри, что с ней сделает твой GC.

emulek
()
Ответ на: комментарий от crowbar

Ну да, точно. Но всё равно, нужен механизм управления памятью получше, чем в C, это уж точно

hvatitbanit
()
Ответ на: комментарий от emulek

Зачем френдить? Лучше ржать с твое самоувереной тупизны.

nanoolinux ★★★★
()

Я наверно что-то не понимаю, но зачем нужны замыкания в нефунцкиональном языке? Как будто ими кто-то пользуется, кроме всяких хацкелов.

comp00 ★★★★
()
Ответ на: комментарий от hvatitbanit

void*

это тут причём?

вика вот так пишет: «Замыкание (англ. closure) в программировании — функция, в теле которой присутствуют ссылки на переменные, объявленные вне тела этой функции и не в качестве её параметров (а в окружающем коде). Говоря другим языком, замыкание — функция, которая ссылается на свободные переменные в своём контексте.»

вот твой пример:

int lol (int a)
{
    int b (int c)
    {
        return a + c;
    }
    return b();
}

функция b ссылается на переменную a, из другой функции, которая не является её параметром.

А ты путаешь с лямбдой и/или функтором. Я не знаю, с чем ты путаешь. В C/C++ такого нет, и быть не может(лямбды есть в C++11, но другие)

emulek
()
Ответ на: комментарий от emulek

В настоящем замыкании переменная а может быть не доступна из того места, где ты вызываешь b

hvatitbanit
()
Ответ на: комментарий от hvatitbanit

Ты думаешь, что если тип указателя поменять, от этого чего-то поменяется.

ты упоролся. Я так не думаю. Просто твои игры с void* к замыканиям в C++ отношения не имеют.

В настоящем замыкании переменная а может быть не доступна из того места, где ты вызываешь b

в C++ это невозможно без костылей. Я не знаю, может придумают что-нить, в gcc любят всякое не нужно пихать.

Дело в том, что в C++ понятие «функция» имеет значение «адрес начала кода функции», и у неё просто нет контекста. Единственное исключение: локальная функция внутри другой функции. Эта фича позволяет работать одной функции в контексте другой(т.е. делать «замыкания»).

А никаким void*'ом ты этого не передашь, потому-что ты вообще ничего кроме адреса не передашь. Тут тебе не void* нужен, а функтор, т.е. класс с пере(за)груженной функцией operator()(). Вот с такой ☣ ты сможешь отобразить нечто похожее на «настоящие замыкания». Ибо только класс может тащить за собой какой-то контекст.

emulek
()
Ответ на: комментарий от emulek

Тут тебе не void* нужен, а функтор,

Ты не скажешь, что ты вообще к нему прицепился-то, а? Ты хотел «замыкание на стеке» (ух лол), я тебе продемонстрировал. И продемонстрировал, как следующий вызов затирает предыдущее содержимое фрейма.

Also: в gcc есть nested functions, которые никакие не замыкания, а nested functions. Ты доволен? Расходимся?

hvatitbanit
()
Ответ на: комментарий от hvatitbanit

Ты хотел «замыкание на стеке»

я? Не, ты меня неправильно понял.

как следующий вызов затирает предыдущее содержимое фрейма.

это говнокод по любому. Обсуждать тут нечего.

в gcc есть nested functions, которые никакие не замыкания, а nested functions. Ты доволен?

формально с их помощью можно делать замыкания. А недоволен я тем, что до сих пор не понимаю, ЗАЧЕМ это надо?

Что-бы в Over9000 раз наступить на те грабли, на которые наступил ТС? Грабли есть в _любом_ ЯП, что это доказывает?

emulek
()
Ответ на: комментарий от emulek

это говнокод по любому. Обсуждать тут нечего.

Епт, это демонстрация.

ЗАЧЕМ это надо?

Я не видел, чтобы это где-то использовалось

hvatitbanit
()
Ответ на: комментарий от emulek

emulek (16.03.2014 13:52:19) Иксперт по бесконечным МТ. Наглядно доказал что ℤ не кольцо.

Ога, как обычно свое определение GC, да?

Kuzy ★★★
()

Если хочется замыканий в С, то можно использовать блоки. Требуется clang или llvm-gcc.

int main() {
    __block int var = 0;
    void (^block)() = ^{ var++; };
    block();
    printf("%d", var);
    return 0;
}

Нужно поставить пакеты llvm, clang, libblocksruntime-dev. Собирать командой

clang main.c -fblocks -lBlocksRuntime -o main

note173 ★★★★★
()
Ответ на: комментарий от Kuzy

Kuzy (16.03.2014 14:55:52) любитель вставлять тупые комменты не в тему.

emulek
()
Ответ на: комментарий от hvatitbanit

Я не видел, чтобы это где-то использовалось

я тоже не видел.

За то Over9000 раз видел, как возвращают указатель из функции, а потом удивляются, почему он то работает, то не работает. Сам тоже так делал, да.

emulek
()
Ответ на: комментарий от comp00

Я наверно что-то не понимаю, но зачем нужны замыкания в нефунцкиональном языке?

Коллбеки передавать по-человечески.

Как будто ими кто-то пользуется, кроме всяких хацкелов.

Замыкания есть в большинстве современных языков.

unsigned ★★★★
()
Ответ на: комментарий от emulek

как возвращают указатель из функции

И чё такого? А если там аллоцировать много (так что вызывающей стороне это делать не удобно)?

hvatitbanit
()
Ответ на: комментарий от hvatitbanit

как возвращают указатель из функции

И чё такого?

плохо это. В C/C++ есть грабли: указатель на стек НИЧЕМ не отличается от указателя на кучу. Их никак не отличить. Есть только ОДИН надёжный путь: не возвращать указатели. А если ты вернул — ты имеешь грабли.

В сишечке — да, придётся с этим смириться. А вот в C++ это решаемо.

emulek
()
Ответ на: комментарий от emulek

Сделав закат солнца вручную, в C++11, в принципе, можно организовать замыкания:

#include <iostream>
#include <functional>
#include <memory>

std::pair<std::function<int (int)>, std::function<int (int)>>
make_counter(int initial_value) {
 auto value = std::make_shared<int>(initial_value);
 return {
  [value](int increment) -> int { return *value += increment; },
  [value](int decrement) -> int { return *value -= decrement; }
 };
};

int main() {
 auto counter = make_counter(42);
 std::cout << counter.first(15)  << '\n';
 std::cout << counter.second(33) << '\n';
 return 0;
};
anonymous
()
Ответ на: комментарий от anonymous

std::pair<std::function<int (int)>, std::function<int (int)>>

надеюсь, мне никогда не придётся поддерживать такой код.

emulek
()
Ответ на: комментарий от anonymous

Какой изощрённый способ делать объекты.

struct Counter {
    int value;
    Counter& inc(int delta) { value += delta; return *this; }
    Counter& dec(int delta) { value -= delta; return *this; }
};

Counter f(int value) {
    return {value};
}

int main() {
    return f(2).dec(4).inc(8).value;
}

Объекты это же фактически и есть замыкания, только с возможностью иметь больше одной функции завязанных на данные, но невозможностью неявно захватывать окружение (нужно явно передавать в конструктор — копией или ссылкой/указателем). В С++11 это допилили объектами std::function (те же данные + один operator(), функтор — частный случай объекта) и сахаром лямбд делающим этот автоматические захват окружения копиями или ссылками.

quasimoto ★★★★
()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.