Ви можете робити ці речі, значною мірою тому, що насправді це не все так складно зробити.
З точки зору компілятора, наявність оголошення функції всередині іншої функції є досить тривіальним для реалізації. Компілятору потрібен механізм, який дозволить оголошенням усередині функцій обробляти інші оголошення (наприклад, int x;
) всередині функції.
Зазвичай він матиме загальний механізм синтаксичного аналізу декларації. Для хлопця, який пише компілятор, насправді взагалі неважливо, чи буде цей механізм викликаний при аналізі коду всередині чи поза іншої функції - це просто декларація, тому коли він бачить достатньо, щоб знати, що є декларація, він викликає частину компілятора, яка займається деклараціями.
Насправді, заборона цих конкретних оголошень всередині функції, ймовірно, додасть додаткової складності, тому що тоді компілятору знадобиться цілком безоплатна перевірка, чи він вже розглядає код у визначенні функції та на основі цього вирішує, дозволити чи заборонити це декларації.
Це залишає питання про те, чим відрізняється вкладена функція. Вкладена функція відрізняється тим, як вона впливає на генерацію коду. У мовах, що дозволяють вкладені функції (наприклад, Pascal), ви зазвичай очікуєте, що код у вкладеній функції має прямий доступ до змінних функції, в яку він вкладений. Наприклад:
int foo() {
int x;
int bar() {
x = 1;
}
}
Без локальних функцій код доступу до локальних змінних досить простий. У типовій реалізації, коли виконання входить у функцію, деякий блок простору для локальних змінних виділяється в стеку. Усі локальні змінні розміщені в цьому єдиному блоці, і кожна змінна трактується як просто зміщення від початку (або кінця) блоку. Наприклад, давайте розглянемо функцію приблизно так:
int f() {
int x;
int y;
x = 1;
y = x;
return y;
}
Компілятор (припускаючи, що він не оптимізував зайвий код) може генерувати код для цього приблизно еквівалентно цьому:
stack_pointer -= 2 * sizeof(int);
x_offset = 0;
y_offset = sizeof(int);
stack_pointer[x_offset] = 1;
stack_pointer[y_offset] = stack_pointer[x_offset];
return_location = stack_pointer[y_offset];
stack_pointer += 2 * sizeof(int);
Зокрема, має одне розташування, що вказує на початок блоку локальних змінних, і весь доступ до локальних змінних є як відступ від цього розташування.
З вкладеними функціями це вже не так - натомість функція має доступ не тільки до власних локальних змінних, але і до змінних, локальних до всіх функцій, у яких вона вкладена. Замість того, щоб просто мати один "stack_pointer", з якого він обчислює зміщення, йому потрібно повернутися вгору по стеку, щоб знайти stack_pointers локально до функцій, в яких він вкладений.
Тепер у тривіальному випадку це не все так страшно - якщо bar
він вкладений всередину foo
, тоді bar
можна просто шукати стек у попередньому вказівнику стека, щоб отримати доступ foo
до змінних. Правда?
Неправильно!Ну, бувають випадки, коли це може бути правдою, але це не обов'язково так. Зокрема,bar
може бути рекурсивним, і в цьому випадку дане викликbar
можливо, доведеться шукати якусь майже довільну кількість рівнів у резервному стеці, щоб знайти змінні оточуючої функції. Взагалі кажучи, вам потрібно зробити одну з двох речей: або ви поміщаєте додаткові дані в стек, щоб він міг здійснити пошук резервної копії стека під час виконання, щоб знайти кадр стека навколишньої функції, або ви фактично передаєте вказівник на фрейм стека навколишньої функції як прихований параметр для вкладеної функції. О, але тут також не обов’язково є лише одна оточуюча функція - якщо ви можете вкласти функції, ви, можливо, можете вкласти їх (більш-менш) довільно глибоко, тому вам слід бути готовими до передачі довільної кількості прихованих параметрів. Це означає, що у вас зазвичай виходить щось на зразок пов’язаного списку кадрів стека з оточуючими функціями,
Це, однак, означає, що доступ до "локальної" змінної може не бути тривіальною справою. Пошук правильного кадру стека для доступу до змінної може бути нетривіальним, тому доступ до змінних оточуючих функцій також (принаймні зазвичай) повільніший, ніж доступ до справді локальних змінних. І, звичайно, компілятор повинен генерувати код для пошуку правильних кадрів стека, доступу до змінних через будь-яку довільну кількість кадрів стека тощо.
Це складність, якої C уникав, забороняючи вкладені функції. Зараз, безумовно, правда, що нинішній компілятор C ++ - це зовсім інший вид звіра від старовинного компілятора C 1970-х. З такими речами, як множинне віртуальне успадкування, компілятор С ++ у будь-якому випадку повинен мати справу з речами того самого загального характеру (тобто пошук розташування змінної базового класу в таких випадках також може бути нетривіальним). На відсотках, підтримка вкладених функцій не додасть особливої складності поточному компілятору C ++ (а деякі, такі як gcc, вже підтримують їх).
У той же час, це також рідко додає великої корисності. Зокрема, якщо ви хочете визначити щось, що діє як функція всередині функції, ви можете використовувати лямбда-вираз. Це насправді створює об'єкт (тобто екземпляр якогось класу), який перевантажує оператор виклику функції ( operator()
), але все одно надає функціональні можливості. Це робить збір (або відсутність) даних із навколишнього контексту більш чітким, що дозволяє йому використовувати існуючі механізми, а не винаходити цілком новий механізм та набір правил для його використання.
Підсумок: хоча спочатку може здатися, що вкладені декларації важкі, а вкладені функції тривіальні, більш-менш навпаки: вкладені функції насправді набагато складніші для підтримки, ніж вкладені декларації.
one
- це визначення функції , два інших - це декларації .