Czy możemy mieć funkcje wewnątrz funkcji w C ++?

225

Mam na myśli coś takiego:

int main() 
{
  void a() 
  {
      // code
  }
  a();

  return 0;
}
Rella
źródło
1
Dlaczego próbujesz to zrobić? Wyjaśnienie twojego celu może pozwolić komuś powiedzieć ci właściwy sposób na osiągnięcie twojego celu.
Thomas Owens,
3
gcc obsługuje funkcje zagnieżdżone jako niestandardowe rozszerzenie. Ale lepiej nie używaj go, nawet jeśli używasz gcc. W trybie C ++ i tak nie jest dostępny.
Sven Marnach,
27
@ Thomas: Ponieważ dobrze byłoby zmniejszyć zakres? Funkcje w funkcjach to zwykła funkcja w innych językach.
Johan Kotlinski,
64
Mówi o funkcjach zagnieżdżonych. Podobnie jak w przypadku kolejnych klas wewnątrz klas, chce zagnieździć funkcję wewnątrz funkcji. Właściwie miałem sytuacje, w których bym to zrobił, gdyby to było możliwe. Istnieją języki (np. F #), które na to pozwalają, i mogę powiedzieć, że może uczynić kod o wiele jaśniejszym, czytelniejszym i łatwiejszym do utrzymania bez zanieczyszczania biblioteki dziesiątkami funkcji pomocniczych, które są bezużyteczne poza ściśle określonym kontekstem. ;)
Mephane
16
@Thomas - zagnieżdżenia może być doskonałym mechanizm do rozbijania złożonych funkcji / algorytmów bez bez wypełnienia bieżącego zakresu funkcje, które nie ogólnego stosowania w zakresie okalającego. Pascal i Ada mają dla nich wspaniałe wsparcie (IMO). To samo dotyczy Scali i wielu innych starych / nowych szanowanych języków. Jak każda inna funkcja, mogą być również nadużywane, ale jest to funkcja programisty. IMO były znacznie bardziej korzystne niż szkodliwe.
luis.espinal

Odpowiedzi:

271

Nowoczesne C ++ - Tak z lambdas!

W aktualnych wersjach c ++ (C ++ 11, C ++ 14 i C ++ 17) możesz mieć funkcje wewnątrz funkcji w postaci lambda:

int main() {
    // This declares a lambda, which can be called just like a function
    auto print_message = [](std::string message) 
    { 
        std::cout << message << "\n"; 
    };

    // Prints "Hello!" 10 times
    for(int i = 0; i < 10; i++) {
        print_message("Hello!"); 
    }
}

Lambdas może również modyfikować zmienne lokalne poprzez ** przechwytywanie przez odniesienie *. Dzięki przechwytywaniu przez referencję lambda ma dostęp do wszystkich zmiennych lokalnych zadeklarowanych w zakresie lambda. Może modyfikować i zmieniać je normalnie.

int main() {
    int i = 0;
    // Captures i by reference; increments it by one
    auto addOne = [&] () {
        i++; 
    };

    while(i < 10) {
        addOne(); //Add 1 to i
        std::cout << i << "\n";
    }
}

C ++ 98 i C ++ 03 - Nie bezpośrednio, ale tak z funkcjami statycznymi wewnątrz klas lokalnych

C ++ nie obsługuje tego bezpośrednio.

To powiedziawszy, możesz mieć lokalne klasy i mogą one mieć funkcje (nie staticlub static), więc możesz to zrobić w pewnym stopniu, choć to trochę kłopot:

int main() // it's int, dammit!
{
  struct X { // struct's as good as class
    static void a()
    {
    }
  };

  X::a();

  return 0;
}

Chciałbym jednak zakwestionować praktykę. Wszyscy wiedzą (cóż, teraz, kiedy to robisz :)) C ++ nie obsługuje funkcji lokalnych, więc są przyzwyczajeni do ich braku. Nie są one jednak używane do tej kludge. Spędziłbym sporo czasu na tym kodzie, aby upewnić się, że tak naprawdę jest tylko tam, aby umożliwić lokalne funkcje. Niedobrze.

sbi
źródło
3
Main przyjmuje również dwa argumenty, jeśli zamierzasz pedantycznie podchodzić do typu zwrotu. :) (A może jest to opcjonalne, ale w dzisiejszych czasach nie jest to zwrot? Nie mogę nadążyć.)
Leo Davidson
3
Jest to po prostu złe - łamie każdą konwencję dobrego, czystego kodu. Nie mogę wymyślić jednego przypadku, w którym jest to dobry pomysł.
Thomas Owens,
19
@Thomas Owens: Dobrze, jeśli potrzebujesz funkcji zwrotnej i nie chcesz za jej pomocą zanieczyszczać innej przestrzeni nazw.
Leo Davidson,
9
@Leo: Standard mówi, że istnieją dwie dopuszczalne formy dla main: int main()iint main(int argc, char* argv[])
John Dibling,
8
Standard mówi int main()i int main(int argc, char* argv[])musi być obsługiwany, a inne mogą być obsługiwane, ale wszystkie mają return int.
JoeG,
260

We wszystkich zamiarach i celach C ++ obsługuje to poprzez lambdas : 1

int main() {
    auto f = []() { return 42; };
    std::cout << "f() = " << f() << std::endl;
}

Tutaj fjest obiekt lambda, który działa jako funkcja lokalna w main. Można określić przechwyty, aby umożliwić tej funkcji dostęp do obiektów lokalnych.

Za kulisami fznajduje się obiekt funkcji (tj. Obiekt typu, który zapewnia an operator()). Typ obiektu funkcji jest tworzony przez kompilator na podstawie lambda.


1 od C ++ 11

Konrad Rudolph
źródło
5
Ach, to fajnie! Nie myślałem o tym. To jest o wiele lepszy niż mój pomysł +1ode mnie.
sbi
1
@sbi: W przeszłości użyłem lokalnych struktur do symulacji tego (tak, wstydzę się siebie). Ale użyteczność jest ograniczona przez fakt, że lokalne struktury nie tworzą zamknięcia, tzn. Nie można uzyskać do nich dostępu do zmiennych lokalnych. Musisz je przekazać i przechowywać jawnie za pomocą konstruktora.
Konrad Rudolph
1
@Konrad: Innym problemem z nimi jest to, że w C ++ 98 nie wolno używać typów lokalnych jako parametrów szablonów. Myślę jednak, że C ++ 1x zniosło to ograniczenie. (A może to C ++ 03?)
sbi
3
@luis: Muszę się zgodzić z Fredem. Nadajesz znaczenie lambdom, których po prostu nie mają (ani w C ++, ani w innych językach, z którymi pracowałem - które nie obejmują Python i Ady, dla przypomnienia). Co więcej, dokonanie tego rozróżnienia po prostu nie ma znaczenia w C ++, ponieważ C ++ nie ma funkcji lokalnych, kropka. Ma tylko lambdy. Jeśli chcesz ograniczyć zakres funkcji podobnej do funkcji, jedynymi opcjami są lambdas lub lokalna struktura wspomniana w innych odpowiedziach. Powiedziałbym, że ten ostatni jest raczej zbyt skomplikowany, aby mógł mieć jakiekolwiek praktyczne znaczenie.
Konrad Rudolph,
2
@AustinWBryan No, lambdas w C ++ to po prostu cukier syntaktyczny dla funktorów i mają zerowy narzut. Gdzieś na tej stronie jest pytanie ze szczegółami.
Konrad Rudolph
42

Klasy lokalne zostały już wspomniane, ale tutaj jest sposób, aby pojawiały się jeszcze bardziej jako funkcje lokalne, wykorzystując przeciążenie operatora () i anonimową klasę:

int main() {
    struct {
        unsigned int operator() (unsigned int val) const {
            return val<=1 ? 1 : val*(*this)(val-1);
        }
    } fac;

    std::cout << fac(5) << '\n';
}

Nie radzę tego używać, to tylko zabawna sztuczka (może zrobić, ale imho nie powinna).


Aktualizacja 2014:

Wraz z pojawieniem się C ++ 11 jakiś czas temu możesz teraz mieć funkcje lokalne, których składnia przypomina trochę JavaScript:

auto fac = [] (unsigned int val) {
    return val*42;
};
Sebastian Mach
źródło
1
Powinno być operator () (unsigned int val), brakuje Ci zestawu nawiasów.
Joe D
1
W rzeczywistości jest to całkowicie rozsądne, jeśli musisz przekazać ten funktor do funkcji lub algorytmu STL, takich jak std::sort()lub std::for_each().
Dima,
1
@Dima: Niestety, w C ++ 03 lokalnie zdefiniowanych typów nie można używać jako argumentów szablonów. C ++ 0x naprawia to, ale zapewnia także znacznie ładniejsze rozwiązania lambdas, więc nadal tego nie zrobiłbyś.
Ben Voigt,
Ups, masz rację. Mój błąd. Ale nadal nie jest to tylko zabawna sztuczka. Byłoby użyteczne, gdyby pozwolono. :)
Dima,
3
Rekursja jest obsługiwana. Nie można jednak użyć autodo zadeklarowania zmiennej. Stroustrup podaje przykład: function<void(char*b, char*e)> rev=[](char*b, char*e) { if( 1<e-b ) { swap( *b, *--e); rev(++b,e); } };do odwrócenia ciągu o podanych wskaźnikach początku i końca.
Tytułowy
17

Nie.

Co próbujesz zrobić?

obejście:

int main(void)
{
  struct foo
  {
    void operator()() { int a = 1; }
  };

  foo b;
  b(); // call the operator()

}
Nim
źródło
2
Należy zauważyć, że podejście do tworzenia instancji klas ma alokację pamięci i dlatego jest zdominowane przez podejście statyczne.
ManuelSchneid3r
14

Począwszy od C ++ 11 możesz używać odpowiednich lambd . Zobacz inne odpowiedzi, aby uzyskać więcej informacji.


Stara odpowiedź: możesz posortować, ale musisz oszukiwać i używać manekina:

void moo()
{
    class dummy
    {
    public:
         static void a() { printf("I'm in a!\n"); }
    };

    dummy::a();
    dummy::a();
}
Leo Davidson
źródło
Nie jestem pewien, czy możesz, chyba że zamiast tego utworzysz obiekt (który dodaje tyle samo szumu, IMO). Chyba że jest coś sprytnego, co możesz zrobić z przestrzeniami nazw, ale nie mogę o tym myśleć i prawdopodobnie nie jest dobrym pomysłem nadużywanie języka bardziej niż to, czym już jesteśmy. :)
Leo Davidson,
Pozbycie się manekina :: jest w jednej z pozostałych odpowiedzi.
Sebastian Mach
8

Jak wspomnieli inni, możesz używać funkcji zagnieżdżonych za pomocą rozszerzeń języka gnu w gcc. Jeśli ty (lub twój projekt) trzymasz się łańcucha narzędziowego gcc, twój kod będzie w większości przenośny w różnych architekturach docelowych kompilatora gcc.

Jeśli jednak istnieje możliwość, że konieczne może być skompilowanie kodu przy użyciu innego zestawu narzędzi, to trzymam się z dala od takich rozszerzeń.


Podczas używania funkcji zagnieżdżonych również ostrożnie podążałem. Są pięknym rozwiązaniem do zarządzania strukturą złożonych, ale spójnych bloków kodu (których fragmenty nie są przeznaczone do użytku zewnętrznego / ogólnego). Są również bardzo pomocne w kontrolowaniu zanieczyszczenia przestrzeni nazw (bardzo realna sprawa z naturalnie złożonymi / długie lekcje w pełnych językach).

Ale jak wszystko, mogą być podatni na nadużycia.

Szkoda, że ​​C / C ++ nie obsługuje takich funkcji jako standard. Większość wariantów Pascala i Ada ma (prawie wszystkie języki oparte na Algolu). To samo z JavaScript. To samo z nowoczesnymi językami, takimi jak Scala. To samo z czcigodnymi językami, takimi jak Erlang, Lisp lub Python.

I podobnie jak w przypadku C / C ++, niestety Java (dzięki której zarabiam większość życia) nie.

Wspominam tutaj o Javie, ponieważ widzę kilka plakatów sugerujących użycie klas i metod klas jako alternatywy dla funkcji zagnieżdżonych. Jest to również typowe obejście w Javie.

Krótka odpowiedź: nie

W ten sposób wprowadza się sztuczną, niepotrzebną złożoność w hierarchii klas. Ponieważ wszystkie rzeczy są równe, idealnym rozwiązaniem jest posiadanie hierarchii klas (i obejmujących przestrzenie nazw i zakresy) reprezentującej rzeczywistą domenę tak prosto, jak to możliwe.

Funkcje zagnieżdżone pomagają radzić sobie z „prywatną” złożonością wewnątrzfunkcyjną. Bez tych udogodnień należy starać się unikać propagowania tej „prywatnej” złożoności do własnego modelu klasowego.

W oprogramowaniu (i dowolnej dyscyplinie inżynierskiej) modelowanie jest kwestią kompromisów. Tak więc w prawdziwym życiu będą uzasadnione wyjątki od tych zasad (a raczej wytycznych). Postępuj jednak ostrożnie.

luis.espinal
źródło
8

Nie możesz mieć funkcji lokalnych w C ++. Jednak C ++ 11 ma lambdas . Lambda to zasadniczo zmienne, które działają jak funkcje.

Lambda ma typ std::function( właściwie to nie do końca prawda , ale w większości przypadków można przypuszczać, że tak). Aby użyć tego typu, musisz #include <functional>. std::functionjest szablonem, przyjmującym jako argument szablonu typ zwracany i typy argumentów ze składnią std::function<ReturnType(ArgumentTypes). Na przykład, std::function<int(std::string, float)>lambda zwraca inti przyjmuje dwa argumenty, jeden std::stringi jeden float. Najczęstszym jest ten std::function<void()>, który nic nie zwraca i nie przyjmuje argumentów.

Po zadeklarowaniu lambda jest ona wywoływana tak jak normalna funkcja, przy użyciu składni lambda(arguments).

Aby zdefiniować lambda, użyj składni [captures](arguments){code}(są na to inne sposoby, ale nie wspomnę o nich tutaj). argumentsto argumenty, które przyjmuje lambda, i codeto kod, który powinien być uruchamiany, gdy lambda jest wywoływana. Zwykle wkładasz [=]lub [&]jako przechwytywanie. [=]oznacza, że ​​przechwytujesz wszystkie zmienne w zakresie, w którym wartość jest zdefiniowana przez wartość, co oznacza, że ​​zachowają wartość, którą mieli, kiedy zadeklarowano lambda. [&]oznacza, że ​​przechwytujesz wszystkie zmienne w zakresie przez odniesienie, co oznacza, że ​​zawsze będą miały swoją bieżącą wartość, ale jeśli zostaną usunięte z pamięci, program się zawiesi. Oto kilka przykładów:

#include <functional>
#include <iostream>

int main(){
    int x = 1;

    std::function<void()> lambda1 = [=](){
        std::cout << x << std::endl;
    };
    std::function<void()> lambda2 = [&](){
        std::cout << x << std::endl;
    };

    x = 2;
    lambda1();    //Prints 1 since that was the value of x when it was captured and x was captured by value with [=]
    lambda2();    //Prints 2 since that's the current value of x and x was captured by value with [&]

    std::function<void()> lambda3 = [](){}, lambda4 = [](){};    //I prefer to initialize these since calling an uninitialized lambda is undefined behavior.
                                                                 //[](){} is the empty lambda.

    {
        int y = 3;    //y will be deleted from the memory at the end of this scope
        lambda3 = [=](){
            std::cout << y << endl;
        };
        lambda4 = [&](){
            std::cout << y << endl;
        };
    }

    lambda3();    //Prints 3, since that's the value y had when it was captured

    lambda4();    //Causes the program to crash, since y was captured by reference and y doesn't exist anymore.
                  //This is a bit like if you had a pointer to y which now points nowhere because y has been deleted from the memory.
                  //This is why you should be careful when capturing by reference.

    return 0;
}

Możesz także uchwycić określone zmienne, podając ich nazwy. Wystarczy podać ich nazwę, aby uchwycić je według wartości, a określenie ich za pomocą &wcześniej spowoduje przechwycenie ich przez odniesienie. Na przykład [=, &foo]przechwyci wszystkie zmienne według wartości, z wyjątkiem tych, fooktóre zostaną przechwycone przez odniesienie, i [&, foo]przechwyci wszystkie zmienne przez odniesienie, z wyjątkiem tych, fooktóre zostaną przechwycone przez wartość. Możesz także przechwytywać tylko określone zmienne, na przykład [&foo]przechwytuje się fooprzez odniesienie i nie przechwytuje żadnych innych zmiennych. Za pomocą można także przechwytywać żadnych zmiennych []. Jeśli spróbujesz użyć zmiennej w lambdzie, której nie przechwyciłeś, nie zostanie skompilowana. Oto przykład:

#include <functional>

int main(){
    int x = 4, y = 5;

    std::function<void(int)> myLambda = [y](int z){
        int xSquare = x * x;    //Compiler error because x wasn't captured
        int ySquare = y * y;    //OK because y was captured
        int zSquare = z * z;    //OK because z is an argument of the lambda
    };

    return 0;
}

Nie można zmienić wartości zmiennej, która została przechwycona przez wartość wewnątrz lambda (zmienne przechwycone przez wartość mają consttyp wewnątrz lambda). Aby to zrobić, musisz uchwycić zmienną przez odniesienie. Oto przykład:

#include <functional>

int main(){
    int x = 3, y = 5;
    std::function<void()> myLambda = [x, &y](){
        x = 2;    //Compiler error because x is captured by value and so it's of type const int inside the lambda
        y = 2;    //OK because y is captured by reference
    };
    x = 2;    //This is of course OK because we're not inside the lambda
    return 0;
}

Ponadto wywoływanie niezainicjowanych lambd jest nieokreślonym zachowaniem i zwykle powoduje awarię programu. Na przykład nigdy nie rób tego:

std::function<void()> lambda;
lambda();    //Undefined behavior because lambda is uninitialized

Przykłady

Oto kod tego, co chcesz zrobić w swoim pytaniu za pomocą lambdas:

#include <functional>    //Don't forget this, otherwise you won't be able to use the std::function type

int main(){
    std::function<void()> a = [](){
        // code
    }
    a();
    return 0;
}

Oto bardziej zaawansowany przykład lambda:

#include <functional>    //For std::function
#include <iostream>      //For std::cout

int main(){
    int x = 4;
    std::function<float(int)> divideByX = [x](int y){
        return (float)y / (float)x;    //x is a captured variable, y is an argument
    }
    std::cout << divideByX(3) << std::endl;    //Prints 0.75
    return 0;
}
Kaczor Donald
źródło
7

Nie, to niedozwolone. Ani C, ani C ++ domyślnie nie obsługują tej funkcji, jednak TonyK wskazuje (w komentarzach), że istnieją rozszerzenia kompilatora GNU C, które umożliwiają takie zachowanie w C.

Thomas Owens
źródło
2
Jest obsługiwany przez kompilator GNU C, jako specjalne rozszerzenie. Ale tylko dla C, a nie C ++.
TonyK
Ach Nie mam żadnych specjalnych rozszerzeń w moim kompilatorze C. Jednak dobrze wiedzieć. Dodam ten titbit do mojej odpowiedzi.
Thomas Owens,
Użyłem rozszerzenia gcc do obsługi funkcji zagnieżdżonych (w C, ale nie w C ++). Funkcje zagnieżdżone to sprytna rzecz (jak w Pascalu i Adzie) do zarządzania złożonymi, ale spójnymi strukturami, które nie są przeznaczone do ogólnego użytku. Tak długo, jak używa się zestawu narzędzi gcc, zapewnione jest, że będzie on w większości przenośny dla wszystkich docelowych architektur. Ale jeśli nastąpi zmiana w konieczności kompilowania wynikowego kodu za pomocą kompilatora innego niż gcc, najlepiej unikać takich rozszerzeń i trzymać się jak najbliżej mantry ansi / posix.
luis.espinal
7

Wszystkie te sztuczki wyglądają (mniej więcej) jako funkcje lokalne, ale tak nie działają. W funkcji lokalnej możesz używać zmiennych lokalnych jej superfunkcji. To rodzaj półglobali. Żadne z tych sztuczek może to zrobić. Najbliższa jest sztuczka lambda z c ++ 0x, ale jej zamknięcie jest ograniczone czasem definicji, a nie czasem użycia.

Royas
źródło
Teraz myślę, że to najlepsza odpowiedź. Chociaż możliwe jest zadeklarowanie funkcji w ramach funkcji (której używam cały czas), nie jest to funkcja lokalna zdefiniowana w wielu innych językach. Nadal dobrze jest wiedzieć o takiej możliwości.
Alexis Wilke,
6

Nie możesz zdefiniować wolnej funkcji wewnątrz innej w C ++.

Prasoon Saurav
źródło
1
Nie z ansi / posix, ale możesz to zrobić z rozszerzeniami GNU.
luis.espinal
4

Pozwól, że opublikuję tutaj rozwiązanie dla C ++ 03, które uważam za najczystsze z możliwych. *

#define DECLARE_LAMBDA(NAME, RETURN_TYPE, FUNCTION) \
    struct { RETURN_TYPE operator () FUNCTION } NAME;

...

int main(){
  DECLARE_LAMBDA(demoLambda, void, (){ cout<<"I'm a lambda!"<<endl; });
  demoLambda();

  DECLARE_LAMBDA(plus, int, (int i, int j){
    return i+j;
  });
  cout << "plus(1,2)=" << plus(1,2) << endl;
  return 0;
}

(*) w świecie C ++ używanie makr nigdy nie jest uważane za czyste.

Barney
źródło
Alexis, masz rację, mówiąc, że nie jest idealnie czysty. Nadal jest prawie czysty, ponieważ dobrze wyraża to, co programista zamierzał zrobić, bez skutków ubocznych. Uważam, że sztuką programowania jest pisanie w sposób czytelny dla człowieka, który czyta się jak powieść.
Barney,
2

Ale możemy zadeklarować funkcję wewnątrz main ():

int main()
{
    void a();
}

Chociaż składnia jest poprawna, czasami może prowadzić do „najbardziej dokuczliwej analizy”:

#include <iostream>


struct U
{
    U() : val(0) {}
    U(int val) : val(val) {}

    int val;
};

struct V
{
    V(U a, U b)
    {
        std::cout << "V(" << a.val << ", " << b.val << ");\n";
    }
    ~V()
    {
        std::cout << "~V();\n";
    }
};

int main()
{
    int five = 5;
    V v(U(five), U());
}

=> brak danych wyjściowych programu.

(Tylko ostrzeżenie Clang po kompilacji).

Ponownie najbardziej dokuczliwa analiza C ++

wyzwalacz
źródło