Können wir Funktionen in Funktionen in C ++ haben?


225

Ich meine so etwas wie:

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

  return 0;
}

1
Warum versuchst du das zu tun? Wenn Sie Ihren Zweck erklären, kann Ihnen möglicherweise jemand sagen, wie Sie Ihr Ziel richtig erreichen können.
Thomas Owens

3
gcc unterstützt verschachtelte Funktionen als nicht standardmäßige Erweiterung. Verwenden Sie es jedoch besser nicht, auch wenn Sie gcc verwenden. Und im C ++ - Modus ist es sowieso nicht verfügbar.
Sven Marnach

27
@ Thomas: Weil es gut wäre, den Umfang eines? Funktionen in Funktionen sind in anderen Sprachen üblich.
Johan Kotlinski

64
Er spricht von verschachtelten Funktionen. Ähnlich wie er Klassen innerhalb von Klassen weiterleiten kann, möchte er eine Funktion innerhalb einer Funktion verschachteln. Eigentlich hatte ich Situationen, in denen ich das auch getan hätte, wenn es möglich gewesen wäre. Es gibt Sprachen (z. B. F #), die dies ermöglichen, und ich kann Ihnen sagen, dass dadurch Code viel klarer, lesbarer und wartbarer wird, ohne eine Bibliothek mit Dutzenden von Hilfsfunktionen zu verschmutzen, die außerhalb eines bestimmten Kontexts nutzlos sind. ;)
Mephane

16
@Thomas - verschachtelte Funktionen können ein hervorragender Mechanismus sein, um komplexe Funktionen / Algorithmen zu unterbrechen, ohne den aktuellen Bereich mit Funktionen zu füllen, die im umschließenden Bereich nicht allgemein von Nutzen sind. Pascal und Ada haben (IMO) eine schöne Unterstützung für sie. Gleiches gilt für Scala und viele andere alte / neue angesehene Sprachen. Wie jede andere Funktion können sie auch missbraucht werden, aber das ist eine Funktion des Entwicklers. IMO, sie waren weitaus vorteilhafter als schädlich.
Luis.espinal

Antworten:


271

Modernes C ++ - Ja mit Lambdas!

In aktuellen Versionen von c ++ (C ++ 11, C ++ 14 und C ++ 17) können Sie Funktionen in Funktionen in Form eines Lambda haben:

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 kann lokale Variablen auch über ** Capture-by-Reference * ändern. Mit der Erfassung durch Referenz hat das Lambda Zugriff auf alle lokalen Variablen, die im Gültigkeitsbereich des Lambda deklariert sind. Es kann sie normal modifizieren und ändern.

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 und C ++ 03 - Nicht direkt, aber ja mit statischen Funktionen in lokalen Klassen

C ++ unterstützt das nicht direkt.

Das heißt, Sie können lokale Klassen haben, und sie können Funktionen haben (nicht staticoder static), so dass Sie dies bis zu einem gewissen Grad erhalten können, obwohl es ein bisschen kludge ist:

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

  X::a();

  return 0;
}

Ich würde jedoch die Praxis in Frage stellen. Jeder weiß (nun, da Sie es sowieso tun :)), dass C ++ keine lokalen Funktionen unterstützt, daher sind sie es gewohnt, sie nicht zu haben. Sie sind jedoch nicht an diesen Kludge gewöhnt. Ich würde eine ganze Weile mit diesem Code verbringen, um sicherzustellen, dass er wirklich nur lokale Funktionen zulässt. Nicht gut.


3
Main benötigt auch zwei Argumente, wenn Sie hinsichtlich des Rückgabetyps pedantisch sein wollen. :) (Oder ist das optional, aber nicht die Rückkehr in diesen Tagen? Ich kann nicht mithalten.)
Leo Davidson

3
Das ist einfach schlecht - es bricht jede Konvention von gutem, sauberem Code. Ich kann mir keinen einzigen Fall vorstellen, in dem dies eine gute Idee ist.
Thomas Owens

19
@ Thomas Owens: Es ist gut, wenn Sie eine Rückruffunktion benötigen und keinen anderen Namespace damit verschmutzen möchten.
Leo Davidson

9
@ Leo: Der Standard besagt, dass es zwei zulässige Formen für main gibt: int main()undint main(int argc, char* argv[])
John Dibling

8
Der Standard sagt int main()und int main(int argc, char* argv[])muss unterstützt werden und andere können unterstützt werden, aber alle haben return int.
JoeG

260

In jeder Hinsicht unterstützt C ++ dies über Lambdas : 1

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

Hier fist ein Lambda-Objekt, das als lokale Funktion in fungiert main. Es können Erfassungen angegeben werden, damit die Funktion auf lokale Objekte zugreifen kann.

Hinter den Kulissen fbefindet sich ein Funktionsobjekt (dh ein Objekt eines Typs, der ein bereitstellt operator()). Der Funktionsobjekttyp wird vom Compiler basierend auf dem Lambda erstellt.


1 seit C ++ 11


5
Ah, das ist ordentlich! Ich habe nicht daran gedacht. Das ist viel besser als meine Idee +1von mir.
sbi

1
@sbi: Ich habe in der Vergangenheit tatsächlich lokale Strukturen verwendet, um dies zu simulieren (ja, ich schäme mich angemessen für mich selbst). Der Nutzen wird jedoch durch die Tatsache eingeschränkt, dass lokale Strukturen keinen Abschluss erzeugen, dh Sie können nicht auf lokale Variablen in ihnen zugreifen. Sie müssen sie explizit über einen Konstruktor übergeben und speichern.
Konrad Rudolph

1
@Konrad: Ein weiteres Problem bei ihnen ist, dass Sie in C ++ 98 keine lokalen Typen als Vorlagenparameter verwenden dürfen. Ich denke, C ++ 1x hat diese Einschränkung jedoch aufgehoben. (Oder war das C ++ 03?)
sbi

3
@luis: Ich muss Fred zustimmen. Sie fügen Lambdas eine Bedeutung hinzu, die sie einfach nicht haben (weder in C ++ noch in anderen Sprachen, mit denen ich gearbeitet habe - ohne Python und Ada). Darüber hinaus ist diese Unterscheidung in C ++ einfach nicht sinnvoll, da C ++ keine lokalen Funktionen hat. Es gibt nur Lambdas. Wenn Sie den Umfang einer funktionsähnlichen Sache auf eine Funktion beschränken möchten, können Sie nur Lambdas oder die in anderen Antworten erwähnte lokale Struktur auswählen. Ich würde sagen, dass Letzteres eher zu kompliziert ist, um von praktischem Interesse zu sein.
Konrad Rudolph

2
@AustinWBryan Nein, Lambdas in C ++ sind nur syntaktischer Zucker für Funktoren und haben keinen Overhead. Irgendwo auf dieser Website gibt es eine ausführlichere Frage.
Konrad Rudolph

42

Lokale Klassen wurden bereits erwähnt, aber hier ist eine Möglichkeit, sie mithilfe einer operator () -Überladung und einer anonymen Klasse noch mehr als lokale Funktionen erscheinen zu lassen:

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

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

Ich rate nicht dazu, es ist nur ein lustiger Trick (kann, aber imho sollte nicht).


Update 2014:

Mit dem Aufstieg von C ++ 11 vor einiger Zeit können Sie jetzt lokale Funktionen haben, deren Syntax ein wenig an JavaScript erinnert:

auto fac = [] (unsigned int val) {
    return val*42;
};

1
Sollte sein operator () (unsigned int val), fehlt Ihnen ein Satz von Klammern.
Joe D

1
Tatsächlich ist dies eine völlig vernünftige Sache, wenn Sie diesen Funktor an eine stl-Funktion oder einen Algorithmus wie std::sort()oder übergeben müssen std::for_each().
Dima

1
@Dima: Leider können in C ++ 03 lokal definierte Typen nicht als Vorlagenargumente verwendet werden. C ++ 0x behebt dies, bietet aber auch die viel schöneren Lösungen von Lambdas, sodass Sie das immer noch nicht tun würden.
Ben Voigt

Ups, du hast recht. Mein Fehler. Trotzdem ist dies nicht nur ein lustiger Trick. Es wäre eine nützliche Sache gewesen, wenn es erlaubt wäre. :)
Dima

3
Rekursion wird unterstützt. Sie können autodie Variable jedoch nicht deklarieren. Stroustrup gibt das Beispiel: function<void(char*b, char*e)> rev=[](char*b, char*e) { if( 1<e-b ) { swap( *b, *--e); rev(++b,e); } };Zum Umkehren einer Zeichenfolge mit Anfangs- und Endzeigern.
Gleichnamiger

17

Nein.

Was versuchst du zu machen?

Problemumgehung:

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

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

}

2
Beachten Sie, dass der Klasseninstanziierungsansatz mit einer Speicherzuweisung ausgestattet ist und daher vom statischen Ansatz dominiert wird.
ManuelSchneid3r

14

Ab C ++ 11 können Sie geeignete Lambdas verwenden . Weitere Details finden Sie in den anderen Antworten.


Alte Antwort: Sie können, irgendwie, aber Sie müssen betrügen und eine Dummy-Klasse verwenden:

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

    dummy::a();
    dummy::a();
}

Ich bin mir nicht sicher, ob Sie dies können, außer indem Sie stattdessen ein Objekt erstellen (was genauso viel Rauschen hinzufügt, IMO). Es sei denn, es gibt eine clevere Sache, die Sie mit Namespaces machen können, aber ich kann mir das nicht vorstellen und es ist wahrscheinlich keine gute Idee, die Sprache mehr zu missbrauchen als das, was wir bereits sind. :)
Leo Davidson

Das Entfernen von Dummy :: ist in einer der anderen Antworten.
Sebastian Mach

8

Wie bereits erwähnt, können Sie verschachtelte Funktionen mithilfe der gnu-Spracherweiterungen in gcc verwenden. Wenn Sie (oder Ihr Projekt) sich an die gcc-Toolchain halten, kann Ihr Code hauptsächlich auf die verschiedenen Architekturen übertragen werden, auf die der gcc-Compiler abzielt.

Wenn es jedoch eine mögliche Anforderung gibt, dass Sie möglicherweise Code mit einer anderen Toolchain kompilieren müssen, würde ich mich von solchen Erweiterungen fernhalten.


Ich würde auch vorsichtig vorgehen, wenn ich verschachtelte Funktionen verwende. Sie sind eine schöne Lösung für die Verwaltung der Struktur komplexer, aber zusammenhängender Codeblöcke (deren Teile nicht für den externen / allgemeinen Gebrauch bestimmt sind). Sie sind auch sehr hilfreich bei der Kontrolle der Verschmutzung von Namespaces (ein sehr reales Problem bei natürlich komplexen /. lange Klassen in ausführlichen Sprachen.)

Aber wie alles andere können sie für Missbrauch offen sein.

Es ist traurig, dass C / C ++ solche Funktionen nicht als Standard unterstützt. Die meisten Pascal-Varianten und Ada tun dies (fast alle Algol-basierten Sprachen). Gleiches gilt für JavaScript. Gleiches gilt für moderne Sprachen wie Scala. Gleiches gilt für ehrwürdige Sprachen wie Erlang, Lisp oder Python.

Und genau wie bei C / C ++ funktioniert Java (mit dem ich den größten Teil meines Lebensunterhalts verdiene) leider nicht.

Ich erwähne Java hier, weil ich mehrere Poster sehe, die die Verwendung von Klassen und Klassenmethoden als Alternativen zu verschachtelten Funktionen vorschlagen. Und das ist auch die typische Problemumgehung in Java.

Kurze Antwort: Nein.

Dies führt tendenziell zu einer künstlichen, unnötigen Komplexität einer Klassenhierarchie. Wenn alle Dinge gleich sind, ist es ideal, eine Klassenhierarchie (und ihre umfassenden Namespaces und Bereiche) zu haben, die eine tatsächliche Domäne so einfach wie möglich darstellt.

Verschachtelte Funktionen helfen bei der Bewältigung der "privaten" Komplexität innerhalb der Funktion. Ohne diese Einrichtungen sollte man versuchen zu vermeiden, diese "private" Komplexität in das eigene Klassenmodell zu übertragen.

In der Software (und in jeder technischen Disziplin) ist die Modellierung eine Frage von Kompromissen. Daher wird es im wirklichen Leben gerechtfertigte Ausnahmen von diesen Regeln (oder vielmehr Richtlinien) geben. Gehen Sie jedoch vorsichtig vor.


8

Sie können keine lokalen Funktionen in C ++ haben. C ++ 11 hat jedoch Lambdas . Lambdas sind im Grunde genommen Variablen, die wie Funktionen funktionieren.

Ein Lambda hat den Typ std::function( eigentlich stimmt das nicht ganz , aber in den meisten Fällen kann man davon ausgehen). Um diesen Typ zu verwenden, müssen Sie #include <functional>. std::functionist eine Vorlage, die als Vorlagenargument den Rückgabetyp und die Argumenttypen mit der Syntax verwendet std::function<ReturnType(ArgumentTypes). Zum Beispiel std::function<int(std::string, float)>gibt ein Lambda ein zurück intund nimmt zwei Argumente an, eins std::stringund eins float. Die häufigste ist std::function<void()>, dass sie nichts zurückgibt und keine Argumente akzeptiert.

Sobald ein Lambda deklariert ist, wird es wie eine normale Funktion unter Verwendung der Syntax aufgerufen lambda(arguments).

Verwenden Sie zum Definieren eines Lambda die Syntax [captures](arguments){code}(es gibt andere Möglichkeiten, dies zu tun, aber ich werde sie hier nicht erwähnen). argumentsDies sind die Argumente, die das Lambda verwendet, und codeder Code, der ausgeführt werden soll, wenn das Lambda aufgerufen wird. Normalerweise setzen Sie [=]oder [&]als Captures. [=]bedeutet, dass Sie alle Variablen in dem Bereich erfassen, in dem der Wert durch value definiert ist. Dies bedeutet, dass sie den Wert beibehalten, den sie hatten, als das Lambda deklariert wurde. [&]bedeutet, dass Sie alle Variablen im Bereich als Referenz erfassen. Dies bedeutet, dass sie immer ihren aktuellen Wert haben. Wenn sie jedoch aus dem Speicher gelöscht werden, stürzt das Programm ab. Hier sind einige Beispiele:

#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;
}

Sie können bestimmte Variablen auch erfassen, indem Sie deren Namen angeben. Wenn Sie nur ihren Namen angeben, werden sie nach Wert erfasst. Wenn Sie ihren Namen mit einem &Vorher angeben, werden sie als Referenz erfasst. Erfasst beispielsweise [=, &foo]alle Variablen nach Wert, außer foodenen, die als Referenz erfasst werden, und [&, foo]erfasst alle Variablen nach Referenz, außer foodie nach Wert erfasst werden. Sie können auch nur bestimmte Variablen erfassen, z. B. [&foo]als fooReferenz und keine anderen Variablen. Sie können mit überhaupt auch überhaupt keine Variablen erfassen []. Wenn Sie versuchen, eine Variable in einem Lambda zu verwenden, die Sie nicht erfasst haben, wird sie nicht kompiliert. Hier ist ein Beispiel:

#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;
}

Sie können den Wert einer Variablen, die durch den Wert in einem Lambda erfasst wurde, nicht ändern (Variablen, die durch den Wert erfasst werden, haben einen constTyp innerhalb des Lambda). Dazu müssen Sie die Variable als Referenz erfassen. Hier ist ein Beispiel:

#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;
}

Das Aufrufen von nicht initialisierten Lambdas ist ein undefiniertes Verhalten und führt normalerweise zum Absturz des Programms. Tun Sie zum Beispiel niemals Folgendes:

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

Beispiele

Hier ist der Code für das, was Sie in Ihrer Frage mit Lambdas tun wollten:

#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;
}

Hier ist ein fortgeschritteneres Beispiel für ein 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;
}

7

Nein, das ist nicht erlaubt. Weder C noch C ++ unterstützen diese Funktion standardmäßig. TonyK weist jedoch (in den Kommentaren) darauf hin, dass es Erweiterungen für den GNU C-Compiler gibt, die dieses Verhalten in C ermöglichen.


2
Es wird vom GNU C-Compiler als spezielle Erweiterung unterstützt. Aber nur für C, nicht für C ++.
TonyK

Ah. Ich habe keine speziellen Erweiterungen in meinem C-Compiler. Das ist aber gut zu wissen. Ich werde diesen Leckerbissen zu meiner Antwort hinzufügen.
Thomas Owens

Ich habe die Erweiterung gcc zur Unterstützung verschachtelter Funktionen verwendet (in C jedoch nicht in C ++). Verschachtelte Funktionen sind eine gute Sache (wie in Pascal und Ada), um komplexe, aber zusammenhängende Strukturen zu verwalten, die nicht von allgemeinem Nutzen sein sollen. Solange man die gcc-Toolchain verwendet, ist sichergestellt, dass sie größtenteils auf alle Zielarchitekturen portierbar ist. Wenn sich jedoch ändert, dass der resultierende Code mit einem Nicht-GCC-Compiler kompiliert werden muss, ist es am besten, solche Erweiterungen zu vermeiden und sich so nah wie möglich an das Ansi / Posix-Mantra zu halten.
Luis.espinal

7

All diese Tricks sehen (mehr oder weniger) nur als lokale Funktionen aus, funktionieren aber nicht so. In einer lokalen Funktion können Sie lokale Variablen ihrer Superfunktionen verwenden. Es ist eine Art Halbglobale. Keiner dieser Tricks kann das. Der nächste ist der Lambda-Trick aus c ++ 0x, aber sein Abschluss ist an die Definitionszeit gebunden, nicht an die Nutzungszeit.


Jetzt denke ich, dass dies die beste Antwort ist. Obwohl es möglich ist, eine Funktion innerhalb einer Funktion zu deklarieren (die ich ständig benutze), handelt es sich nicht um eine lokale Funktion, wie sie in vielen anderen Sprachen definiert ist. Es ist immer noch gut, die Möglichkeit zu kennen.
Alexis Wilke


4

Lassen Sie mich hier eine Lösung für C ++ 03 veröffentlichen, die ich für möglichst sauber halte. *

#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;
}

(*) In der C ++ - Welt wird die Verwendung von Makros niemals als sauber angesehen.


Alexis, du hast Recht zu sagen, dass es nicht perfekt sauber ist. Es ist immer noch fast sauber, da es gut ausdrückt, was der Programmierer vorhatte, ohne Nebenwirkungen. Ich denke, die Kunst des Programmierens besteht darin, menschlich lesbar ausdrucksstark zu schreiben, das sich wie ein Roman liest.
Barney

2

Aber wir können eine Funktion in main () deklarieren:

int main()
{
    void a();
}

Obwohl die Syntax korrekt ist, kann sie manchmal zur "ärgerlichsten Analyse" führen:

#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());
}

=> keine Programmausgabe.

(Nur Clang-Warnung nach der Kompilierung).

Wieder die nervigste Analyse von C ++

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.