Możesz robić te rzeczy, głównie dlatego, że nie są one wcale takie trudne.
Z punktu widzenia kompilatora, umieszczenie deklaracji funkcji wewnątrz innej funkcji jest dość proste do zaimplementowania. Kompilator potrzebuje mechanizmu, aby zezwalać deklaracjom wewnątrz funkcji na obsługę innych deklaracji (np. int x;
) Wewnątrz funkcji i tak.
Zazwyczaj ma ogólny mechanizm analizowania deklaracji. Dla faceta piszącego kompilator nie ma znaczenia, czy ten mechanizm jest wywoływany podczas analizowania kodu wewnątrz, czy na zewnątrz innej funkcji - to tylko deklaracja, więc kiedy widzi wystarczająco dużo, aby wiedzieć, co jest deklaracją, wywołuje część kompilatora, która zajmuje się deklaracjami.
W rzeczywistości zakazanie tych konkretnych deklaracji wewnątrz funkcji prawdopodobnie zwiększyłoby złożoność, ponieważ kompilator potrzebowałby wówczas całkowicie nieuzasadnionego sprawdzenia, czy już patrzy na kod wewnątrz definicji funkcji i na tej podstawie decyduje, czy zezwolić, czy zabronić tego konkretnego deklaracja.
Pozostaje pytanie, czym różni się funkcja zagnieżdżona. Zagnieżdżona funkcja różni się tym, jak wpływa na generowanie kodu. W językach, które zezwalają na funkcje zagnieżdżone (np. Pascal), normalnie oczekuje się, że kod funkcji zagnieżdżonej ma bezpośredni dostęp do zmiennych funkcji, w której jest zagnieżdżona. Na przykład:
int foo() {
int x;
int bar() {
x = 1;
}
}
Bez funkcji lokalnych kod dostępu do zmiennych lokalnych jest dość prosty. W typowej implementacji, gdy wykonanie wchodzi do funkcji, na stosie alokowany jest pewien blok miejsca na zmienne lokalne. Wszystkie zmienne lokalne są alokowane w tym pojedynczym bloku, a każda zmienna jest traktowana jako po prostu przesunięcie od początku (lub końca) bloku. Na przykład rozważmy funkcję podobną do tej:
int f() {
int x;
int y;
x = 1;
y = x;
return y;
}
Kompilator (zakładając, że nie zoptymalizował dodatkowego kodu) może wygenerować kod odpowiadający z grubsza temu:
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);
W szczególności ma jeden lokalizację wskazującą początek bloku zmiennych lokalnych, a cały dostęp do zmiennych lokalnych jest przesunięty z tej lokalizacji.
W przypadku funkcji zagnieżdżonych tak już nie jest - zamiast tego funkcja ma dostęp nie tylko do swoich własnych zmiennych lokalnych, ale także do zmiennych lokalnych dla wszystkich funkcji, w których jest zagnieżdżona. Zamiast mieć tylko jeden „stack_pointer”, na podstawie którego oblicza przesunięcie, musi przejść z powrotem w górę stosu, aby znaleźć wskaźniki stack_pointers lokalne dla funkcji, w których jest zagnieżdżony.
Teraz, w trywialnym przypadku, który też nie jest aż tak straszny - jeśli bar
jest zagnieżdżony w środku foo
, bar
może po prostu spojrzeć na stos na poprzedni wskaźnik stosu, aby uzyskać dostęp do foo
zmiennych. Dobrze?
Źle! Cóż, są przypadki, w których może to być prawdą, ale niekoniecznie tak jest. W szczególności,bar
może być rekurencyjny, w którym to przypadku dane wywołaniebar
być może trzeba będzie spojrzeć na prawie dowolną liczbę poziomów na stosie, aby znaleźć zmienne otaczającej funkcji. Ogólnie rzecz biorąc, musisz zrobić jedną z dwóch rzeczy: albo umieścić dodatkowe dane na stosie, aby mógł przeszukiwać stos w czasie wykonywania, aby znaleźć ramkę stosu otaczającej funkcji, albo efektywnie przekażesz wskaźnik do ramka stosu funkcji otaczającej jako ukryty parametr funkcji zagnieżdżonej. Och, ale niekoniecznie istnieje też tylko jedna otaczająca funkcja - jeśli możesz zagnieżdżać funkcje, prawdopodobnie możesz zagnieżdżać je (mniej lub bardziej) dowolnie głęboko, więc musisz być gotowy do przekazania dowolnej liczby ukrytych parametrów. Oznacza to, że zazwyczaj kończy się coś w rodzaju listy ramek stosu połączonych z otaczającymi funkcjami,
Oznacza to jednak, że dostęp do zmiennej „lokalnej” może nie być sprawą trywialną. Znalezienie właściwej ramki stosu, aby uzyskać dostęp do zmiennej, może być nietrywialne, więc dostęp do zmiennych otaczających funkcji jest również (przynajmniej zwykle) wolniejszy niż dostęp do zmiennych prawdziwie lokalnych. I, oczywiście, kompilator musi wygenerować kod, aby znaleźć odpowiednie ramki stosu, uzyskać dostęp do zmiennych za pośrednictwem dowolnej liczby ramek stosu i tak dalej.
To jest złożoność, której C unikał, zakazując zagnieżdżonych funkcji. Z pewnością prawdą jest, że obecny kompilator C ++ jest raczej innym rodzajem bestii niż klasyczny kompilator C. z lat 70. W przypadku dziedziczenia wielokrotnego, wirtualnego, kompilator C ++ musi w każdym przypadku radzić sobie z rzeczami o tej samej ogólnej naturze (tj. Znalezienie lokalizacji zmiennej klasy bazowej w takich przypadkach również może być nietrywialne). W ujęciu procentowym obsługa funkcji zagnieżdżonych nie zwiększyłaby zbytnio złożoności obecnego kompilatora C ++ (a niektóre, takie jak gcc, już je obsługują).
Jednocześnie rzadko dodaje też wiele użyteczności. W szczególności, jeśli chcesz zdefiniować coś, co działa jak funkcja wewnątrz funkcji, możesz użyć wyrażenia lambda. To, co to faktycznie tworzy, to obiekt (tj. Instancja jakiejś klasy), który przeciąża operator wywołania funkcji ( operator()
), ale nadal daje funkcje podobne do funkcji. Sprawia jednak, że przechwytywanie (lub nie) danych z otaczającego kontekstu jest bardziej wyraźne, co pozwala mu korzystać z istniejących mechanizmów zamiast wymyślać zupełnie nowy mechanizm i zestaw reguł ich użycia.
Konkluzja: nawet jeśli początkowo mogłoby się wydawać, że deklaracje zagnieżdżone są trudne, a funkcje zagnieżdżone są trywialne, prawdą jest mniej więcej na odwrót: funkcje zagnieżdżone są w rzeczywistości znacznie bardziej skomplikowane w obsłudze niż deklaracje zagnieżdżone.
one
to definicja funkcji , pozostałe dwie to deklaracje .