Вложенные функции на C++
Я наткнулся на возможность сделать в С++ что-то похожее на объявление функций внутри функций. Выглядит это вот так:
В приведенном примере внутри метода main изготавливается вложенный «метод» с названием make_hello и затем вызывается с параметром «Vasiliy Pupkin». Разумеется, на экран будет выведено Hello, Vasiliy Pupkin! .
К сожалению, перетащить название вверх у меня не получилось.
Сделано это, конечно же, на макросах.
Примеры того же самого без макросов
В C++ все-таки нету вложенных функций, поэтому нам придется эмулировать их синтаксис чем-либо еще. Поэтому зададимся другим вопросом: что именно выглядит так же, как функция, но не функция?
Ответа целых два: это, во-первых, вызов конструктора класса без с созданием объекта «в пустоту»:
К сожалению, есть два существенных «но», которые не позволяют использовать приведенный пример на практике:
1. Мы создаем по одному новому объекту на каждый вызов функции, что не есть хорошо. А ну как у меня цикл на много вызовов? Накладные расходы можно стерпеть на факт декларации такой штуковины, но никак не на использование.
2. Visual Studio со включенной оптимизацией компилятора просто-напросто вырежет создание inline_function() как бесполезное. Логика компилятора понятна (все равно этот создаваемый объект никто не будет использовать, так зачем его создавать?) но представляет опасность.
Однако есть еще штука под названием «переопределение операторов» — и мы можем переопределить двойные скобочки (по умному переопределенный оператор () называется «функтором», во как):
В этом примере уже почти все хорошо, кроме того, что класс можно сделать и анонимным, чтобы попусту не трепать имя вида inline_function_class
Пишем макросы
Итак, цель — в принципе — достигнута: у нас есть способ изолировать кусок кода внутри одного метода так, чтобы не подпортить внешнюю зону видимости. Но есть один минус: во всех приведенных примерах — на мой вкус — слишком много букв. Поэтому мы разделим наш код на три части:
Первую и вторую части можно запихнуть в два макроса, дав им какие-нибудь красивые имена. Например,
При помощи этих макросов код «вложенной функции» значительно упрощается на вид (хоть и выглядит не в С++ стиле):
К сожалению, все портится, если попробовать задать не один параметр у функции, а хотя бы два. В этом случае компилятор нажалуется, мол, в макросе inline_function всего ОДИН параметр и баста. Проблему можно решить, использовав макрос __VA_ARGS__ (который, хоть и не входит в стандарт, но поддерживается всеми реально используемыми компиляторами).
Чуть сложнее, но все еще приемлимо будет выглядеть функция с двумя параметрами:
В заключение отмечу, что у этих макросов не предусмотрено возвращаемого значения. Разумеется, его можно и очень просто «пробросить наверх», так что это я оставляю читателям.
Способы передачи функции(функтора) в качестве аргумента другой функции
Наткнулся тут на вопрос про std::bind и возник вопрос.
Чем отличается способ с шаблоном
Какие возможности и ограничения у того или иного способа, когда их нужно применять.
![]()
Первый способ передает функтор в метод статически — что позволяет компилятору заинлайнить его вызов, если функтор достаточно короткий.
Второй способ позволяет избавиться от шаблонов — но ценой вызова виртуального метода; такой метод уже не получится заинлайнить в общем случае (тем не менее, компилятор все еще может заинлайнить его если перед этим заинлайнил display ).
Третий способ позволяет передать только функцию, но не функтор.
![]()
Первый вариант принимает любой объект, не проверяя его тип. Так, если туда передать что-то, что не может быть вызвано, или что-то, что возвращает тип, который не может быть использован в cout , то получите не совсем релевантную ошибку компиляции.
Второй вариант принимает любой объект, который может быть вызван. При это согласованность сигнатур проверяется сразу и выдаётся релевантная ошибка, что позволяет быстрее понять, где проблема.
Третий вариант принимает лишь свободные функции и лямбды без состояний и является более ограниченным чем второй вариант. Использовать третий вариант в современном C++ коде нет никакого смысла.
Касательно применения: стоит всегда использовать std::function , за исключением тех моментов, когда это по каким-то причинам вредит.
Передача функции в функцию
Передача функции в функцию в C++ осуществляется с помощью указателей на функцию.
Рассмотрим для начала пример.
В первую очередь нас интересует строка, где в функцию передаётся другая функция:
Здесь, в качестве параметра function выступает
Фактически, здесь используется так называемый указатель на функцию и подразумевается
Но вместо (*f) компилятор понимает и привычное f.
Аналогично, можно передавать в функции и другие функции с самыми разнообразными определениями.
В программе из примера в функцию передавались математические функции из файла math.h — sin, cos, exp, а также, функция, созданная пользователем — f1.
Функции внутри функций в Си

Если вкратце, то по-другому nested функцию не объявить.

auto
Defines a local variable as having a local lifetime.
Keyword auto uses the following syntax:
[auto] data-definition;
As the local lifetime is the default for local variables, auto keyword is extremely rarely used.
Note: GNU C extends auto keyword to allow forward declaration of nested functions.
A nested function always has internal linkage. Declaring one with extern is erroneous. If you need to declare the nested function before its definition, use auto (which is otherwise meaningless for function declarations).
Что-то в последнее время на лоре все реже и реже вспоминают про маргинальные языки. Чаще вспоминают С и C++, вот она суровая реальность.
Просто почти всех интересных людей выгнали.

4.4 Вложенные Функции
Вложенная функция — это функция, определенная внутри другой функции. Имя вложенной функции является локальным в блоке, где она определена. Например, здесь мы определяем вложенную функцию с именем square и вызываем ее дважды:
Определения вложенных функций разрешаются внутри функций, где допустимы определения переменных; то есть в любом блоке перед первым оператором в блоке.
нафик такой изврат ? какие то костыли что мы имеем ? ограничение области видимости глобальных переменных используемых обеими функциями но разве не для этого предназначены глобальные переменные с внутренним связыванием и отдельная единица трансляции ? зачем вводить лишние сущности ? тем более такие костыли так как реально они не нужны ну разве что паскалистам перешедшим на си нехватает такой хрени
Вот это отлично, теперь я могу писать каллбеки к g_hash_table_foreach прямо в функции. Я и не знал про такую фичу.
Лучше так не делай.

>нафик такой изврат ?
перед коллегами понтоваться же.
для работы за 15+ лет ни разу не надо было. просто как-то встретил в чужом (быдло)коде.
А я частенько внутри функций делаю вложенные inline-функции, если хочется улучшить читабельность кода и не копипастить 100500раз одно и то же.

>А я частенько внутри функций делаю вложенные inline-функции, если хочется улучшить читабельность кода и не копипастить 100500раз одно и то же.
ИМХО можно сделать внешнюю неинлайновую функцию. gcc сейчас умеет их инлайтить как я понял.
а никто не смотрел куда компилятор пихает разделяемые переменные ? и как вложенная функция до них дотягивается ? как глобальные переменные не может быть из за многопоточности тогда выходит вложенная функция как то хитро дотягивается в стековый фрейм выше
Это лишнее, если inline-функция должна использовать кучу временных переменных функции, из которой вызывается, а писать 100500 аргументов не хочется.
Если inline-функция ничего не возвращает, ее, конечно, проще всего вообще как макрос оформить (а чтобы знать, к чему этот макрос относится, удобно его определить прямо внутри той функции, из которой inline’ы вызываются).
Чуть мозг не закипел, пока пытался понять. Так и не понял.

вложенные функции не входят в стандарт, это расширение GCC => kill it with fire.
> вложенные функции не входят в стандарт, это расширение GCC => kill it with fire.
Разве это в стандарт c99 не входит?
Да, а если они в стандарт не входят, можно при помощи временных переменных и макросов их реализовать 🙂
Самое забавное когда вложенная функция передается как аргумент. Она и в этом случае может дотягиваться до локальных переменных «основной» функции. Так как никакого «нормального» способа передать контекст в точку вызова в этом случае нет, то просто генерируется фрагмент кода для данного конкретного вызова, именно адрес оного кода (содержащий загрузку адреса стека основной функции) передается как аргумент. Работает, но изврат однако.
Система защиты ЛОРа достала.

>Да, а если они в стандарт не входят, можно при помощи временных переменных и макросов их реализовать 🙂
и усложнить себе-же жизнь 🙁
Чем? Наоборот, вместо кучи копипасты с незначительными различиями, имеем несколько строчек.

Можно писать замыкания, это чертовски удобно. Правда в Си не так удобно, слишком много всего писать, типы описывать надо. И коллеги могут не понять.

enjoy your functional programming 🙂
Вообще забавно, возьму на заметку.
> Чуть мозг не закипел, пока пытался понять. Так и не понял.
Нормальный вопрос человек поднял, а то что вы его не поняли говорит о вашем низком уровне развития. Чтобы повысить его, почитайте про то как в языке Си организуются вызовы функций, тогда может быть поймете в чем проблема создания вложенных функций в языке Си.

>Чем? Наоборот, вместо кучи копипасты с незначительными различиями, имеем несколько строчек.
с этим я не спорю. вот только не все компиляторы такое поддерживают, ваш код будет непереносим. Я лучше займусь сейчас копипастой, чем потом буду переделывать. Благо современные редакторы (vim, emacs, даже kate) отлично копипастят.
Т.е. вы считаете, что это предложение написано на русском языке
как глобальные переменные не может быть из за многопоточности тогда выходит вложенная функция как то хитро дотягивается в стековый фрейм выше
> Т.е. вы считаете, что это предложение написано на русском языке
Согласен, что предложение у него написано криво, тем не менее общий смысл вопроса понять можно.
Не, я в тарабарском не разбираюсь 🙂
С пунктуацией беда просто.
А никто не смотрел, куда компилятор пихает разделяемые переменные? И как вложенная функция до них дотягивается? Как глобальные переменные — не может быть из-за многопоточности; тогда выходит вложенная функция как-то хитро дотягивается в стековый фрейм выше.
[quote]Самое забавное когда вложенная функция передается как аргумент. Она и в этом случае может дотягиваться до локальных переменных «основной» функции. Так как никакого «нормального» способа передать контекст в точку вызова в этом случае нет, то просто генерируется фрагмент кода для данного конкретного вызова, именно адрес оного кода (содержащий загрузку адреса стека основной функции) передается как аргумент.[/quote]
Proof в студию. 3 года назад gcc никаких лексических замыканий не делал. С трудом верится, что сейчас что-то подобное есть.
С примером нет проблем.
Вот не знаю как его перекосит LOR.
Дабы было не скучно использовал допотопную систему
gcc 3.2 (2002 год) и gdb (2001):
(gdb) l
1 void xxx(int (*)());
2
3 int bimbom()
4 {
5 int a = 0;
6 int tiptop()
7 {
8 a = 1;
9 }
10 xxx(tiptop);
(gdb)
11 return a;
12 }
13
14 main()
15 {
16 printf («%d\n», bimbom());
17 }
18
19 void xxx(int (*f)())
20 {
(gdb)
21 f();
22 }
23
24
(gdb) b bimbom
Breakpoint 1 at 0x4010d0: file proof.c, line 10.
(gdb) run
Starting program: proof
Breakpoint 1, bimbom () at proof.c:10
10 xxx(tiptop);
(gdb) s
5 int a = 0;
(gdb)
10 xxx(tiptop);
(gdb)
xxx (f=0x22fd90) at proof.c:21
21 f();
(gdb) si
0x0022fd90 in ?? ()
(gdb) disas
Этот код был сгенерирован в стеке, сейчас несколько иначе.
Dump of assembler code from 0x22fd90 to 0x22fdd0:
0x22fd90: mov $0x22fda0,%ecx
0x22fd95: jmp 0x401090 <tiptop.0>
_tiptop.0:
pushl %ebp
movl %esp, %ebp
subl $4, %esp
movl %ecx, -4(%ebp)
movl $1, -4(%ecx)
movl %ebp, %esp
popl %ebp
ret
_bimbom:
pushl %ebp
movl %esp, %ebp
leal -24(%ebp), %eax
Генерация кода в стеке:
movl $_tiptop.0-10, %edx
leal -8(%ebp), %ecx
subl %eax, %edx
subl $40, %esp
movl %edx, 6(%eax)
movb $-71, (%eax)
movl %ecx, 1(%eax)
movb $-23, 5(%eax)
movl %eax, (%esp)
movl $0, -12(%ebp)
call _xxx
movl -12(%ebp), %eax
movl %ebp, %esp
popl %ebp
.
Рекомендую попробовать в новом — отличия будут, но принцип
trampoline сохранен.
Где здесь генерация «кода»? Вижу оффсеты
Здесь нет «генерации кода на стеке», больше похоже на обновление смещений в сегменте кода.
Состояние памяти с будущим кодом в момент начала выполнения bimbom:
(gdb) disas 0x22fd90 0x22fd90+10
Dump of assembler code from 0x22fd90 to 0x22fd9a:
0x22fd90: add %al,(%eax)
0x22fd92: add %al,(%eax)
0x22fd94: add %al,(%eax)
0x22fd96: add %al,(%eax)
0x22fd98: test $0xfd,%al
End of assembler dump.
(gdb) disas
Dump of assembler code for function bimbom:
0x4010b0 <bimbom>: push %ebp
0x4010b1 <bimbom+1>: mov %esp,%ebp
0x4010b3 <bimbom+3>: lea 0xffffffe8(%ebp),%eax
0x4010b6 <bimbom+6>: mov $0x401086,%edx
0x4010bb <bimbom+11>: lea 0xfffffff8(%ebp),%ecx
0x4010be <bimbom+14>: sub %eax,%edx
0x4010c0 <bimbom+16>: sub $0x28,%esp
0x4010c3 <bimbom+19>: mov %edx,0x6(%eax)
0x4010c6 <bimbom+22>: movb $0xb9,(%eax)
0x4010c9 <bimbom+25>: mov %ecx,0x1(%eax)
0x4010cc <bimbom+28>: movb $0xe9,0x5(%eax)
0x4010d0 <bimbom+32>: mov %eax,(%esp,1)
0x4010d3 <bimbom+35>: movl $0x0,0xfffffff4(%ebp)
0x4010da <bimbom+42>: call 0x401120 <xxx>
0x4010df <bimbom+47>: mov 0xfffffff4(%ebp),%eax
0x4010e2 <bimbom+50>: mov %ebp,%esp
0x4010e4 <bimbom+52>: pop %ebp
0x4010e5 <bimbom+53>: ret
End of assembler dump.
(gdb) b *0x4010da
Breakpoint 2 at 0x4010da: file proof.c, line 10.
(gdb) c
Continuing.
Breakpoint 2, bimbom () at proof.c:10
10 xxx(tiptop);
Состояние той же памяти перед вызовом xxx:
(gdb) disas 0x22fd90 0x22fd90+10
Dump of assembler code from 0x22fd90 to 0x22fd9a:
0x22fd90: mov $0x22fda0,%ecx <<< обеcпечение доступа
0x22fd95: jmp 0x401090 <tiptop.0>
End of assembler dump.
Можно найти байты команд, которые генерируются выше (0xb9, 0xe9):
(gdb) x/16bx 0x22fd90
0x22fd90: 0xb9 0xa0 0xfd 0x22 0x00 0xe9 0xf6 0x12
0x22fd98: 0x1d 0x00 0x22 0x00 0x00 0x00 0x00 0x00
(gdb) p $sp
$3 = (void *) 0x22fd80