To nie jest kwestia funkcji lambda, wiem, że mogę przypisać lambdę do zmiennej.
Jaki jest sens pozwalania nam deklarować, ale nie definiować funkcji w kodzie?
Na przykład:
#include <iostream>
int main()
{
// This is illegal
// int one(int bar) { return 13 + bar; }
// This is legal, but why would I want this?
int two(int bar);
// This gets the job done but man it's complicated
class three{
int m_iBar;
public:
three(int bar):m_iBar(13 + bar){}
operator int(){return m_iBar;}
};
std::cout << three(42) << '\n';
return 0;
}
Chcę więc wiedzieć, dlaczego C ++ miałby zezwalać na to, two
co wydaje się bezużyteczne i three
co wydaje się znacznie bardziej skomplikowane, ale nie zezwala one
?
EDYTOWAĆ:
Z odpowiedzi wynika, że deklaracja w kodzie może być w stanie zapobiec zanieczyszczeniu przestrzeni nazw, ale miałem nadzieję, że dowiem się, dlaczego możliwość deklarowania funkcji jest dozwolona, ale możliwość definiowania funkcji jest niedozwolona.
c++
functor
function-declaration
Jonathan Mee
źródło
źródło
one
to definicja funkcji , pozostałe dwie to deklaracje .Odpowiedzi:
Nie jest oczywiste, dlaczego
one
nie jest to dozwolone; funkcje zagnieżdżone zostały zaproponowane dawno temu w N0295, który mówi:Oczywiście ta propozycja została odrzucona, ale ponieważ nie mamy protokołu ze spotkania dostępnego online
1993
, ponieważ nie mamy możliwego źródła uzasadnienia tego odrzucenia.W rzeczywistości ta propozycja jest odnotowana w wyrażeniach lambda i domknięciach dla C ++ jako możliwa alternatywa:
Biorąc pod uwagę, że mamy teraz lambdy, jest mało prawdopodobne, abyśmy zobaczyli funkcje zagnieżdżone, ponieważ, jak zarysowano w artykule, są one alternatywami dla tego samego problemu, a funkcje zagnieżdżone mają kilka ograniczeń w stosunku do lambd.
Jeśli chodzi o tę część twojego pytania:
Istnieją przypadki, w których byłby to przydatny sposób wywołania żądanej funkcji. Wersja robocza sekcji standardowej C ++
3.4.1
[basic.lookup.unqual] podaje jeden interesujący przykład:namespace NS { class T { }; void f(T); void g(T, int); } NS::T parm; void g(NS::T, float); int main() { f(parm); // OK: calls NS::f extern void g(NS::T, float); g(parm, 1); // OK: calls g(NS::T, float) }
źródło
::g(parm, 1)
w celu wywołania funkcji w globalnej przestrzeni nazw? Lub sprawdź,g(parm, 1.0f);
co powinno dać lepsze dopasowanie do pożądanegog
?Cóż, odpowiedź brzmi „z powodów historycznych”. W C możesz mieć deklaracje funkcji w zakresie blokowym, a projektanci C ++ nie widzieli korzyści z usunięcia tej opcji.
Przykładowe użycie to:
#include <iostream> int main() { int func(); func(); } int func() { std::cout << "Hello\n"; }
IMO to zły pomysł, ponieważ łatwo o błąd, dostarczając deklarację niezgodną z rzeczywistą definicją funkcji, co prowadzi do niezdefiniowanego zachowania, które nie zostanie zdiagnozowane przez kompilator.
źródło
some_type f();
i definicję w innej jednostce tłumaczeniowejanother_type f() {...}
. Kompilator nie może ci powiedzieć, że te nie pasują, a wywołanief
z niewłaściwą deklaracją da niezdefiniowane zachowanie. Dlatego dobrym pomysłem jest mieć tylko jedną deklarację w nagłówku i dołączyć ten nagłówek, w którym funkcja jest zdefiniowana, a także miejsce, w którym jest używana.W podanym przykładzie,
void two(int)
jest deklarowana jako funkcja zewnętrzna, przy czym ta deklaracja jest ważna tylko w zakresiemain
funkcji .Jest to rozsądne, jeśli chcesz, aby nazwa była
two
dostępna tylko w domeniemain()
, aby uniknąć zanieczyszczenia globalnej przestrzeni nazw w bieżącej jednostce kompilacji.Przykład w odpowiedzi na komentarze:
main.cpp:
int main() { int foo(); return foo(); }
foo.cpp:
int foo() { return 0; }
nie ma potrzeby plików nagłówkowych. kompilować i łączyć z
skompiluje się i uruchomi, a program zwróci 0 zgodnie z oczekiwaniami.
źródło
two
również zdefiniowane w pliku, powodując w ten sposób zanieczyszczenie?two()
można zdefiniować w zupełnie innej jednostce kompilacji.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; // Should assign to the `x` defined in `foo`. } }
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); // allocate space for local variables x_offset = 0; y_offset = sizeof(int); stack_pointer[x_offset] = 1; // x = 1; stack_pointer[y_offset] = stack_pointer[x_offset]; // y = x; return_location = stack_pointer[y_offset]; // return y; 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 środkufoo
,bar
może po prostu spojrzeć na stos na poprzedni wskaźnik stosu, aby uzyskać dostęp dofoo
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.
źródło
Pierwsza to definicja funkcji i jest niedozwolona. Oczywiście wt to użycie umieszczenia definicji funkcji wewnątrz innej funkcji.
Ale pozostałe dwójki to tylko deklaracje. Wyobraź sobie, że musisz użyć
int two(int bar);
funkcji w głównej metodzie. Ale jest zdefiniowana poniżejmain()
funkcji, więc deklaracja funkcji wewnątrz funkcji sprawia, że możesz używać tej funkcji z deklaracjami.To samo dotyczy trzeciego. Deklaracje klasy wewnątrz funkcji pozwalają na użycie klasy wewnątrz funkcji bez podawania odpowiedniego nagłówka lub odwołania.
int main() { // This is legal, but why would I want this? int two(int bar); //Call two int x = two(7); class three { int m_iBar; public: three(int bar):m_iBar(13 + bar) {} operator int() {return m_iBar;} }; //Use class three *threeObj = new three(); return 0; }
źródło
Ta funkcja języka została odziedziczona z C, gdzie służyła do jakiegoś celu we wczesnych latach C (może określanie zakresu deklaracji funkcji?) . Nie wiem, czy ta funkcja jest często używana przez współczesnych programistów C i szczerze w to wątpię.
Podsumowując odpowiedź:
nie ma celu dla tej funkcji we współczesnym C ++ (o którym przynajmniej wiem), jest tutaj z powodu wstecznej kompatybilności C ++ - to-C (chyba :)).
Dzięki poniższemu komentarzowi:
Prototyp funkcji jest ograniczony do funkcji, w której jest zadeklarowany, więc można mieć bardziej uporządkowaną globalną przestrzeń nazw - odwołując się do zewnętrznych funkcji / symboli bez
#include
.źródło
W rzeczywistości istnieje jeden przypadek użycia, który jest prawdopodobnie przydatny. Jeśli chcesz mieć pewność, że zostanie wywołana określona funkcja (a Twój kod się skompiluje), bez względu na to, co deklaruje otaczający kod, możesz otworzyć własny blok i zadeklarować w nim prototyp funkcji. (Inspiracja pochodzi od Johannesa Schauba, https://stackoverflow.com/a/929902/3150802 , przez TeKa, https://stackoverflow.com/a/8821992/3150802 ).
Może to być szczególnie przydatne, jeśli musisz dołączyć nagłówki, których nie kontrolujesz, lub jeśli masz makro wielowierszowe, które może być używane w nieznanym kodzie.
Kluczem jest to, że deklaracja lokalna zastępuje poprzednie deklaracje w najbardziej wewnętrznym otaczającym bloku. Chociaż może to wprowadzać subtelne błędy (i, jak sądzę, jest zabronione w C #), może być używane świadomie. Rozważać:
// somebody's header void f(); // your code { int i; int f(); // your different f()! i = f(); // ... }
Linkowanie może być interesujące, ponieważ są szanse, że nagłówki należą do biblioteki, ale myślę, że możesz dostosować argumenty linkera, aby
f()
została rozwiązana dla twojej funkcji do czasu rozważenia tej biblioteki. Albo każesz mu ignorować zduplikowane symbole. Lub nie umieszczasz linków do biblioteki.źródło
f
zdefiniowany w twoim przykładzie? Czy nie skończyłbym z błędem redefinicji funkcji, ponieważ różnią się one tylko typem zwracania?void f()
iwint f()
C ++, ponieważ zwracana wartość funkcji nie jest częścią podpisu funkcji w C ++. Zmień drugą deklarację na,int f(int)
a wycofam swój głos przeciw.i = f();
po zadeklarowaniuvoid f()
. „Bez rozróżnienia” to tylko połowa prawdy ;-). Właściwie użyłem „sygnatur” funkcji, które nie są przeciążalne, ponieważ w przeciwnym razie cała okoliczność byłaby niepotrzebna w C ++, ponieważ dwie funkcje z różnymi typami parametrów / numerami mogłyby szczęśliwie współistnieć.Nie jest to odpowiedź na pytanie PO, ale raczej odpowiedź na kilka uwag.
Nie zgadzam się z tymi punktami w komentarzach i odpowiedziach: 1 że deklaracje zagnieżdżone są rzekomo nieszkodliwe, oraz 2 że definicje zagnieżdżone są bezużyteczne.
1 Głównym kontrprzykładem rzekomej nieszkodliwości zagnieżdżonych deklaracji funkcji jest niesławna Najbardziej irytująca analiza . IMO rozprzestrzenianie się zamieszania spowodowanego przez to wystarczy, aby uzasadnić dodatkową regułę zabraniającą zagnieżdżonych deklaracji.
2 Pierwszym kontrprzykładem na rzekomą bezużyteczność zagnieżdżonych definicji funkcji jest częsta potrzeba wykonania tej samej operacji w kilku miejscach wewnątrz dokładnie jednej funkcji. Istnieje oczywiste obejście tego problemu:
private: inline void bar(int abc) { // Do the repeating operation } public: void foo() { int a, b, c; bar(a); bar(b); bar(c); }
Jednak to rozwiązanie często zanieczyszcza definicję klasy licznymi funkcjami prywatnymi, z których każda jest używana dokładnie w jednym wywołującym. Deklaracja funkcji zagnieżdżonej byłaby znacznie czystsza.
źródło
W szczególności odpowiadając na to pytanie:
Ponieważ rozważ ten kod:
int main() { int foo() { // Do something return 0; } return 0; }
Pytania do projektantów języków:
foo()
być dostępne dla innych funkcji?int main(void)::foo()
?źródło
Chciałem tylko podkreślić, że kompilator GCC umożliwia deklarowanie funkcji wewnątrz funkcji. Przeczytaj więcej na ten temat tutaj . Również wraz z wprowadzeniem lambd do C ++, to pytanie jest teraz nieco przestarzałe.
Możliwość zadeklarowania nagłówków funkcji w innych funkcjach, okazała się przydatna w następującym przypadku:
void do_something(int&); int main() { int my_number = 10 * 10 * 10; do_something(my_number); return 0; } void do_something(int& num) { void do_something_helper(int&); // declare helper here do_something_helper(num); // Do something else } void do_something_helper(int& num) { num += std::abs(num - 1337); }
Co my tu mamy? Zasadniczo masz funkcję, która powinna być wywoływana z main, więc to, co robisz, to deklarowanie jej jako normalnej. Ale potem zdajesz sobie sprawę, że ta funkcja potrzebuje również innej funkcji, która pomoże jej w tym, co robi. Więc zamiast deklarować tę funkcję pomocniczą powyżej main, deklarujesz ją wewnątrz funkcji, która jej potrzebuje, a następnie można ją wywołać z tej funkcji i tylko z tej funkcji.
Chodzi mi o to, że deklarowanie nagłówków funkcji wewnątrz funkcji może być pośrednią metodą hermetyzacji funkcji, która pozwala funkcji ukryć niektóre części tego, co robi, delegując do innej funkcji, której tylko ona jest świadoma, prawie dając złudzenie zagnieżdżonego funkcji .
źródło
Deklaracje funkcji zagnieżdżonych są dozwolone prawdopodobnie dla 1. Odwołań do przodu 2. Aby móc zadeklarować wskaźnik do funkcji i przekazać inne funkcje w ograniczonym zakresie.
Definicje funkcji zagnieżdżonych są niedozwolone prawdopodobnie z powodu problemów takich jak 1. Optymalizacja 2. Rekursja (zamykanie i zagnieżdżanie zdefiniowanych funkcji) 3. Ponowne wejście 4. Współbieżność i inne problemy z dostępem do wielu wątków.
Z mojego ograniczonego zrozumienia :)
źródło