Was ist die Lebensdauer eines C ++ - Lambda-Ausdrucks?


70

(Ich habe bereits gelesen, wie lange implizite Funktoren aus Lambda in C ++ noch leben, und diese Frage wird nicht beantwortet.)

Ich verstehe, dass die C ++ - Lambda-Syntax nur Zucker ist, um eine Instanz einer anonymen Klasse mit einem Aufrufoperator und einem bestimmten Status zu erstellen, und ich verstehe die Lebensdaueranforderungen dieses Status (entschieden, ob Sie nach dem Wert von als Referenz erfassen). Aber was ist die Lebensdauer des Lambda-Objekts selbst? std::functionWird die zurückgegebene Instanz im folgenden Beispiel nützlich sein?

std::function<int(int)> meta_add(int x) {
    auto add = [x](int y) { return x + y; };
    return add;
}

Wenn ja, wie funktioniert es ? Das scheint mir ein bisschen zu magisch zu sein - ich kann mir nur vorstellen, dass es funktioniert, wenn std::functionich meine gesamte Instanz kopiere, was je nach dem, was ich aufgenommen habe, sehr schwer sein kann - in der Vergangenheit habe ich std::functionhauptsächlich mit bloßen Funktionszeigern verwendet und diese kopiert schnell. Es scheint auch angesichts der std::functionArt der Löschung problematisch zu sein .

Antworten:


64

Die Lebensdauer ist genau so, wie es wäre, wenn Sie Ihr Lambda durch einen handgerollten Funktor ersetzen würden:

struct lambda {
   lambda(int x) : x(x) { }
   int operator ()(int y) { return x + y; }

private:
   int x;
};

std::function<int(int)> meta_add(int x) {
   lambda add(x);
   return add;
}

Das Objekt wird lokal für die meta_addFunktion erstellt und dann [in seiner Gesamtheit, einschließlich des Werts von x] in den Rückgabewert verschoben. Dann verlässt die lokale Instanz den Gültigkeitsbereich und wird wie gewohnt zerstört. Das von der Funktion zurückgegebene Objekt bleibt jedoch so lange gültig wie das std::functionObjekt, das es enthält. Wie lange das dauert, hängt natürlich vom aufrufenden Kontext ab.


1
Die Sache ist, ich war mir nicht bewusst, dass dies tatsächlich mit benannten Klassen funktioniert, und es verwirrt mich bis zum Äußersten, dass dies der Fall ist.

4
Wenn Sie nicht wissen, wie Funktionsobjekte vor C ++ 11 funktionierten, sollten Sie sich diese ansehen, da Lambdas fast nichts anderes als Syntaxzucker über Funktionsobjekte sind. Sobald Sie verstehen, dass es offensichtlich wird, dass Lambdas dieselbe Wertesemantik wie Funktionsobjekte haben und daher ihre Lebensdauer dieselbe ist.
Bames53

Normale Lebensdauer (auf dem Stapel zugewiesen), aber mit der Rückgabewertoptimierung?
Steve314

Würde das Hinzufügen bei der Rücksendung nicht kopiert, nicht verschoben? Würde ein echtes Lambda bewegt werden? Ich weiß keinen Grund, warum es nicht sein kann, aber vielleicht funktioniert es nicht so?
Allyourcode

18

Es scheint, dass Sie verwirrter sind std::functionals Lambdas.

std::functionverwendet eine Technik namens Typlöschung. Hier ist ein kurzer Vorbeiflug.

class Base
{
  virtual ~Base() {}
  virtual int call( float ) =0;
};

template< typename T>
class Eraser : public Base
{
public:
   Eraser( T t ) : m_t(t) { }
   virtual int call( float f ) override { return m_t(f); }
private:
   T m_t;
};

class Erased
{
public:
   template<typename T>
   Erased( T t ) : m_erased( new Eraser<T>(t) ) { }

   int do_call( float f )
   {
      return m_erased->call( f );
   }
private:
   Base* m_erased;
};

Warum sollten Sie den Typ löschen wollen? Ist das nicht der Typ, den wir wollen int (*)(float)?

Durch das Löschen des Typs Erasedkann jetzt jeder aufrufbare Wert gespeichert werden int(float).

int boring( float f);
short interesting( double d );
struct Powerful
{
   int operator() ( float );
};

Erased e_boring( &boring );
Erased e_interesting( &interesting );
Erased e_powerful( Powerful() );
Erased e_useful( []( float f ) { return 42; } );

3
Rückblickend war ich verwirrt über std :: function, weil ich nicht wusste, dass es das Eigentum an vielem von irgendetwas behält. Ich hatte angenommen, dass das "Umschließen" einer Instanz in eine std :: -Funktion nicht gültig war, nachdem diese Instanz den Gültigkeitsbereich verlassen hatte. Der Grund, warum Lambdas die Verwirrung auf den Kopf gestellt hat, ist, dass std :: function im Grunde die einzige Möglichkeit ist, sie weiterzugeben (wenn ich einen benannten Typ hätte, würde ich einfach eine Instanz des benannten Typs zurückgeben, und das funktioniert ziemlich offensichtlich). und dann hatte ich keine Ahnung, wohin die Instanz ging.

2
Dies ist nur ein Beispielcode, bei dem einige Details fehlen. Es verliert Speicher und es fehlen Anrufe an std::move, std::forward. Auch std::functionverwendet in der Regel ein kleines Objekt Optimierung mit dem Heap zu vermeiden , wenn Tklein ist .
Deft_code

1
Entschuldigung, ich versuche zu lernen, was für ein Typ Löschung ist, und in Ihrem Beispiel in der Klasse Erased haben Sie m_erased.call (f). Wenn m_erased ein Mitgliedszeiger ist, wie können Sie m_erased.call (f) ausführen? Ich habe versucht, den Punkt in einen Pfeil zu ändern, und ich denke, er versucht, auf die reine virtuelle Basisfunktion zuzugreifen. Ist das, weil es nur ein Beispiel ist? Ich habe es zehn Minuten lang angestarrt und ich denke, ich werde verrückt. Danke
Zebrafisch

1
@TitoneMaurice: Ja, das sollte definitiv ein sein ->. Warum wird Ihrer Meinung nach die virtuelle Basisfunktion aufgerufen? Denken Sie daran, dass eine virtuelle Funktion überladen ist, auch wenn die abgeleitete Klasse das virtualSchlüsselwort weglässt
Eric

12

Das ist:

[x](int y) { return x + y; };

Ist gleichbedeutend mit: (oder kann auch berücksichtigt werden)

struct MyLambda
{
    MyLambda(int x): x(x) {}
    int operator()(int y) const { return x + y; }
private:
    int x;
};

Ihr Objekt gibt also ein Objekt zurück, das genau so aussieht. Welches hat einen gut definierten Kopierkonstruktor. Es erscheint daher sehr vernünftig, dass es korrekt aus einer Funktion kopiert werden kann.


Um es aus einer Funktion zu kopieren, muss die std :: -Funktion ihren Typ kennen, nachdem die std :: -Funktion instanziiert wurde. Wie kann es das machen? Der einzige Trick, der mir in den Sinn kommt, ist ein Zeiger auf eine Instanz einer Vorlagenklasse mit einer virtuellen Funktion, die den genauen Typ des Lambda kennt. Das scheint ziemlich böse zu sein, und ich weiß nicht einmal, ob es wirklich funktionieren würde.

2
@ Joe: Du hast im Grunde genommen deine Löschung vom Typ Mühle beschrieben, und genau so funktioniert es.
Dennis Zickefoose

1
@ JoeWreschnig: Du hast nicht gefragt, wie das std::functionfunktioniert; Sie fragten, wie Lambdas funktionieren. std::functionist nicht dasselbe; Es ist nur eine Möglichkeit, ein generisches Callable in ein Objekt zu verpacken.
Nicol Bolas

1
@NicolBolas: Nun, ich bin aus einem bestimmten Grund über eine std :: -Funktion zurückgekehrt, weil das der Schritt ist, den ich nicht verstanden habe. Wie Dennis sagte, funktioniert dies auch für benannte Klassen, die mir nicht bekannt waren - für ungefähr das letzte Jahr (nachdem ich angefangen hatte, std :: function zu verwenden, aber bevor ich anfing, Lambdas zu verwenden) habe ich immer angenommen, dass es nicht funktionieren würde.

Das Lambda wird in die std::function<>Instanz verschoben und nicht kopiert. Daher ist es irrelevant, einen genau definierten Kopierkonstruktor zu haben - der Verschiebungskonstruktor ist relevant.
ildjarn

4

In dem Code, den Sie gepostet haben:

std::function<int(int)> meta_add(int x) {
    auto add = [x](int y) { return x + y; };
    return add;
}

Das std::function<int(int)>von der Funktion zurückgegebene Objekt enthält tatsächlich eine verschobene Instanz des Lambda-Funktionsobjekts, das der lokalen Variablen zugewiesen wurde add.

Wenn Sie ein C ++ 11-Lambda definieren, das nach Wert oder nach Referenz erfasst, generiert der C ++ - Compiler automatisch einen eindeutigen Funktionstyp, dessen Instanz erstellt wird, wenn das Lambda aufgerufen oder einer Variablen zugewiesen wird. Zur Veranschaulichung generiert Ihr C ++ - Compiler möglicherweise den folgenden Klassentyp für das Lambda, definiert durch [x](int y) { return x + y; }:

class __lambda_373s27a
{
    int x;

public:
    __lambda_373s27a(int x_)
        : x(x_)
    {
    }

    int operator()(int y) const {
        return x + y;
    }
};

Dann ist die meta_addFunktion im Wesentlichen äquivalent zu:

std::function<int(int)> meta_add(int x) {
    __lambda_373s27a add = __lambda_373s27a(x);
    return add;
}

EDIT: Ich bin mir übrigens nicht sicher, ob Sie das wissen, aber dies ist ein Beispiel für das Currying von Funktionen in C ++ 11.


Tatsächlich enthält das von der Funktion zurückgegebene Objekt std::function<int(int)>eine verschobene Instanz des Lambda-Funktionsobjekts - es werden keine Kopien ausgeführt.
ildjarn

ildjarn: Müsste die meta_add(int)Funktion nicht zurückkehren std::move(add), um den Verschiebungskonstruktor des Funktionstyps ( __lambda_373s27ain diesem Fall) aufzurufen ?
Daniel Trebbien

3
Nein, returnAnweisungen dürfen den zurückgegebenen Wert implizit als r-Wert behandeln, wodurch er implizit beweglich wird und die Notwendigkeit eines expliziten Werts entfällt return std::move(...);(wodurch RVO / NRVO verhindert wird und tatsächlich return std::move(...);ein Anti-Pattern erstellt wird). Da adddies in der returnAnweisung als r-Wert behandelt wird , wird das Lambda folglich in das std::function<>Konstruktorargument verschoben .
ildjarn
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.